feat: add drag and drop wizard (#39)

* feat(drag-and-drop): add drag and drop wizard

* chore(seo): update seo share image url

* improvement(upload): add download step 3

* improvement(loading): improve loading and add transitions

* improvement: add pwa support, clipboard and animations

* improvement(seo): add html lang and update title

* fix(button): fix button font family
This commit is contained in:
Rui Saraiva 2020-04-19 19:11:20 +01:00 committed by GitHub
parent 8efa541fd4
commit efa6b4cc1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1960 additions and 338 deletions

View File

@ -1,3 +1,14 @@
{
"presets": ["next/babel"]
"presets": ["next/babel"],
"plugins": [
[
"prismjs",
{
"languages": ["markup"],
"plugins": [],
"theme": "default",
"css": false
}
]
]
}

View File

@ -3,5 +3,6 @@
"semanticCommits": true,
"stabilityDays": 3,
"prCreation": "not-pending",
"labels": ["Type: Chore"]
"labels": ["Type: Chore"],
"baseBranches": ["develop"]
}

View File

@ -0,0 +1,3 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=1" />
<link rel="icon" type="image/png" href="/favicon.png?v=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico?v=1" />

View File

@ -9,5 +9,5 @@
}
.sb-show-main.dark-mode {
background: #191a1f;
background: #101114;
}

View File

@ -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
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
<link rel="apple-touch-icon" sizes="57x57" href="/favicon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180x180.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<meta name="msapplication-config" content="browserconfig.xml">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
<meta name="msapplication-TileImage" content="/favicon-144x144.png">
<meta name="msapplication-config" content="/browserconfig.xml">
<!-- SVG favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- PWA compatible -->
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#ffffff">
```
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).

View File

@ -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;
}
}

View File

@ -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<React.ButtonHTMLAttributes<HTMLButtonElement>, 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 (
<button className={className} {...props}>
<button {...props} className={className}>
{children}
</button>
)
@ -33,6 +43,8 @@ const Button = ({
Button.defaultProps = {
variant: 'primary',
weight: 'bold',
color: 'black',
background: '',
}
export { Button }

View File

@ -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;
}

View File

@ -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 (
<div style={{ padding: 20 }}>
<Checkbox name="test" id="test" disabled={disabled}>
<Typography variant="regularBody" weight="medium" muted={disabled}>
Test Checkbox
</Typography>
</Checkbox>
<br />
<Typography>dark mode: {darkMode.value ? 'enabled' : 'disabled'}</Typography>
</div>
)
}
ToggleDesktop.story = {
name: 'Desktop',
}

View File

@ -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<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & CheckboxProps) => {
const [value, setValue] = useState(false)
const onCheckChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(!value)
if (props.onChange) {
props.onChange(event)
}
}
return (
<div className={styles.root}>
<input
type="checkbox"
{...props}
className={styles.checkbox}
name={name}
id={id}
checked={value}
onChange={onCheckChange}
/>
<label htmlFor={id} className={classnames(styles.label, { [styles.disabled]: props.disabled })}>
<span className={styles.checkboxSvg}>
<SvgCheckbox checked={value} disabled={props.disabled} />{' '}
</span>
<div>{children}</div>
</label>
</div>
)
}
export { Checkbox }

View File

@ -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;
}
}

View File

@ -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 (
<pre className={styles.root}>
<code className="language-markup" dangerouslySetInnerHTML={{ __html: htmlCode }}></code>
</pre>
)
}
export { CodeHighlight }

View File

@ -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;
}

View File

@ -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 (
<div style={{ padding: 20 }}>
<DragAndDrop onDrop={onDrop} />
<DragAndDrop onGenerate={onGenerate} onError={onError} onFile={onFile} />
<br />
<Button onClick={toggle}>Toggle Dark Mode</Button>
</div>

View File

@ -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<ArrayBuffer>
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<File>()
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<ArrayBuffer>()
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 (
<div className={classnames(styles.root, { [styles.dark]: isDark })}>
<div className={classnames(styles.dropZoneWrapper, { [styles.dark]: isDark })}>
<SvgDropZone className={classnames(styles.dashed, { [styles.dragActive]: isDragActive })} />
<div {...getRootProps()} className={styles.dropZone}>
<input {...getInputProps()} />
<div className={styles.imageUpload}>
<SvgImageUpload />
</div>
<Typography
variant="regularBody"
weight="medium"
className={classnames(styles.imageUploadText, { [styles.dark]: isDark })}>
Drag &amp; drop a .png file or
<br />
<u>click here to upload it</u>.
</Typography>
</div>
<div className={classnames(styles.container, { [styles.loading]: isLoading })}>
{!image && !zipData && (
<>
<div className={classnames(styles.dropZoneWrapper, { [styles.dark]: isDark })}>
<SvgDropZone
className={classnames(styles.dashed, { [styles.dragActive]: isDragActive, [styles.dark]: isDark })}
/>
<div {...getRootProps()} className={styles.dropZone}>
<input {...getInputProps()} />
<div className={styles.imageUpload}>
<SvgImageUpload active={isDragActive} />
</div>
<Typography
variant="regularBody"
weight="medium"
className={classnames(styles.imageUploadText, { [styles.dark]: isDark })}>
Drag &amp; drop an image file or
<br />
<u>click here to upload it</u>.
</Typography>
</div>
</div>
<Typography variant="regularBody" weight="medium" className={styles.info}>
<SvgInfo /> We recommend a square <strong>PNG</strong> or <strong>SVG</strong> with at least 310px
</Typography>
<Typography variant="regularBody" weight="medium" className={styles.info}>
<SvgInfo /> No data or images are stored
</Typography>
</>
)}
{image && !zipData && (
<>
<div className={styles.imagePreviewWrapper}>
<div className={classnames(styles.imagePreview, { [styles.notSquare]: !isSquare })}>
<img src={imageBgBase64} alt="Favicon preview background" />
<img src={imageBase64} alt="Favicon preview" />
</div>
<div className={styles.imagePreviewInfo}>
<Typography variant="title" weight="bold" className={styles.filename}>
{title}
</Typography>
<Typography
variant="footer"
weight="semiBold"
color="gray"
tag="div"
className={styles.filenameWrapper}>
<span className={styles.filename}>
{imageData.name}
{imageData.extension}
</span>
<span className={styles.fileSize}>
({imageSizes.width}x{imageSizes.height})
</span>
</Typography>
</div>
</div>
<div className={classnames(styles.imageInfo, { [styles.dark]: isDark })}>
<div className={styles.imageInfoItem}>
<span>{isSquare ? <SvgCheck /> : <SvgError />}</span>
<Typography variant="regularBody" weight="medium">
Square image
</Typography>
</div>
<div className={styles.imageInfoItem}>
<span>{isPngOrSvg ? <SvgCheck /> : <SvgError />}</span>
<Typography variant="regularBody" weight="medium">
PNG or SVG
</Typography>
</div>
<div className={styles.imageInfoItem}>
<span>{isSvg || is310px ? <SvgCheck disabled={isSvg} /> : <SvgError />}</span>
<Typography variant="regularBody" weight="medium" muted={isSvg}>
310px or higher
</Typography>
</div>
</div>
<div className={classnames(styles.imageOptionsWrapper, { [styles.dark]: isDark })}>
<Typography variant="footer" weight="semiBold" color="gray">
Advanced
</Typography>
<div className={styles.imageOptions}>
<div className={styles.imageInfoItem}>
<Checkbox name="pwa" id="pwa" disabled={PWADisabled} onChange={() => setPwa(!pwa)}>
<Typography variant="regularBody" weight="medium" muted={PWADisabled}>
PWA compatible
</Typography>
<Typography variant="footer" weight="semiBold" color="green" muted={PWADisabled || isSvg}>
512px or higher{' '}
<SvgCheck className={classnames(styles.pwaCheck, { [styles.hide]: PWADisabled || isSvg })} />
</Typography>
</Checkbox>
</div>
<div className={styles.imageInfoItem}>
<Checkbox name="dark" id="dark" disabled onChange={() => setDarkMode(!darkMode)}>
<Typography variant="regularBody" weight="medium" muted>
Dark Mode version
</Typography>
<Typography variant="footer" weight="semiBold" color="green" muted>
Available soon
</Typography>
</Checkbox>
</div>
</div>
</div>
<div className={classnames(styles.imageFooter, { [styles.dark]: isDark })}>
<Button variant="transparent" color="gray" onClick={resetImage}>
Re-upload
</Button>
<Button
color="white"
background="bgLink"
className={styles.imageGenerate}
onClick={() => generateFavicon(image)}>
Generate Favicon
</Button>
</div>
</>
)}
{zipData && (
<>
<div>
<div className={styles.imagePreviewDownload}>
<div className={classnames(styles.preview, styles.circle)}>
<img src={imageBase64} alt="Favicon preview circle" />
</div>
<div className={classnames(styles.preview, styles.square)}>
<img src={imageBase64} alt="Favicon preview square" />
</div>
<div className={classnames(styles.preview, styles.rounded)}>
<img src={imageBase64} alt="Favicon preview rounded" />
</div>
</div>
<Typography variant="title" weight="bold" className={styles.imagePreviewTitle}>
Your new favicon is ready!
</Typography>
<Typography variant="footer" weight="semiBold" color="gray" className={styles.imagePreviewSubtitle}>
Weve prepared a zip file with all the sizes and a README with the code plus other resources.
</Typography>
</div>
<div className={classnames(styles.imageInfo, { [styles.dark]: isDark })}>
<div className={styles.imageInfoItem}>
<SvgInfo className={styles.imageInfoItemSvg} />
<Typography variant="regularBody" weight="medium">
{sizesCount} different sizes
</Typography>
</div>
<div className={styles.imageInfoItem}>
<SvgInfo className={styles.imageInfoItemSvg} />
<Typography variant="regularBody" weight="medium">
Code generated
</Typography>
<Button
className={styles.showCode}
variant="regularTransparent"
color="link"
onClick={() => setIsModalOpen(true)}>
Show
</Button>
</div>
</div>
<div className={classnames(styles.imageFooter, styles.spaceBetween, { [styles.dark]: isDark })}>
<Button variant="transparent" color="link" onClick={resetImage}>
Make a new one
</Button>
<Button
color="white"
background="bgGreen"
className={styles.imageGenerate}
onClick={() => onDownload(zipData)}>
Download Favicon
</Button>
</div>
<Modal
ariaHideApp={false}
isOpen={isModalOpen}
onRequestClose={() => setIsModalOpen(false)}
className={styles.content}
overlayClassName={classnames(styles.overlay, { [styles.dark]: isDark })}
closeTimeoutMS={200}
contentLabel="Code generated">
<div className={styles.modalContainer}>
<div className={styles.modalHeader}>
<Typography variant="title" weight="bold">
Insert the following code in the &lt;head&gt; section of your pages:
</Typography>
<Clipboard component="div" data-clipboard-text={headTemplate(undefined, isSvg, pwa)}>
<Button color="white" background="bgLink">
Copy code
</Button>
</Clipboard>
</div>
<div className={styles.modalCode}>
<CodeHighlight template={headTemplate(undefined, isSvg, pwa)} />
</div>
</div>
</Modal>
</>
)}
</div>
<Typography variant="regularBody" weight="medium" className={styles.info}>
<SvgInfo /> We recommend a square png image with at least 310px
</Typography>
<Typography variant="regularBody" weight="medium" className={styles.info}>
<SvgInfo /> No data or images are stored
</Typography>
</div>
)
}

View File

@ -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;
}

View File

@ -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 (
<div className={styles.root}>
<ToolTitle>Favycon</ToolTitle>
<Typography variant="largeBody" weight="medium" className={styles.firstParagraph}>
A small online tool to help you generate your favicon in all the sizes and formats you need.
</Typography>
<Typography variant="largeBody" weight="medium" className={styles.secondParagraph}>
Just drag &amp; drop an image and you will then get a downloadable file alongside some documentation on how to
add the favicons.
</Typography>
<hr />
<Typography variant="footer" weight="semiBold" color="gray">
Created by <a href="/#">4 people</a> on their 2020s worldwide quarantine. Background image from{' '}
<a href={unsplashImagesUrls[imageIndex]} target="_blank" rel="noopener noreferrer">
Unsplash
</a>
.
</Typography>
</div>
)
}
export { FavyconInfo }

View File

@ -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;
}
}

View File

@ -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 (
<div className={styles.root}>
<div className={classnames(styles.error, { [styles.hide]: !error })}>
<Typography variant="regularBody" color="white" weight="semiBold">
{error}
</Typography>
<SvgError className={styles.errorIcon} transparent onClick={clearError} />
</div>
<div className={styles.background}>
<LazyImage
src={`/images/unsplash-${backgroundId}@1x.jpg`}
srcRetina={`/images/unsplash-${backgroundId}@2x.jpg`}
srcPlaceholder={`/images/unsplash-${backgroundId}@placeholder.jpg`}
alt="Unsplash"
aspectRatio="540/354"
/>
</div>
{children}
<div className={classnames(styles.image, { [styles.hide]: !showDndImage })}>
<LazyImage
src={`/images/dnd-${isDark ? 'dark' : 'light'}@1x.png`}
srcRetina={`/images/dnd-${isDark ? 'dark' : 'light'}@2x.png`}
srcPlaceholder={`/images/dnd-${isDark ? 'dark' : 'light'}@1x.png`}
alt="Drag and drop here!"
aspectRatio="184/108"
/>
</div>
</div>
)
}
export const FavyconWizard = React.memo(FavyconWizardComponent)

View File

@ -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);
}

View File

@ -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 = () => {
<div className={styles.container}>
<header className={styles.header}>
<div className={itemClassName}>
<Typography weight="bold">Menu</Typography>
<Link href="/">
<a href="/">
<SvgFavycon />
</a>
</Link>
</div>
<div className={itemClassName}>
<DarkModeToggle />

View File

@ -10,16 +10,14 @@ const SEO = ({ title, description }: SEOProps) => (
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=1" />
<link rel="icon" type="image/png" href="/favicon.png?v=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico?v=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=3" />
<link rel="icon" type="image/png" href="/favicon.png?v=3" />
<link rel="icon" type="image/x-icon" href="/favicon.ico?v=3" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={title} />
<meta property="og:url" content="https://favycon.now.sh" />
<meta property="og:image" content="https://favycon.now.sh/share.png?v=1" />
<meta property="og:image:secure_url" content="https://favycon.now.sh/share.png?v=1" />
<meta property="og:image" content="/share.png?v=2" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

View File

@ -4,7 +4,7 @@ import { uniqueId } from 'utils/ids'
const SvgAppycon: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (props) => {
const idPrefix = uniqueId('svg-')
return (
<svg width={64} height={64} aria-labelledby={`${idPrefix}-title`} {...props}>
<svg width={64} height={64} viewBox="0 0 64 64" aria-labelledby={`${idPrefix}-title`} {...props}>
<title id={`${idPrefix}-title`}>Appycon logo</title>
<defs>
<linearGradient x1="50%" y1="99.731%" x2="50%" y2=".44%" id={`${idPrefix}-c`}>

View File

@ -6,7 +6,7 @@ const SvgArrowDown: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElem
const { value: isDark } = useDarkMode(false)
const idPrefix = uniqueId('svg-')
return (
<svg width={16} height={16} aria-labelledby={`${idPrefix}-title`} {...props}>
<svg width={16} height={16} viewBox="0 0 16 16" aria-labelledby={`${idPrefix}-title`} {...props}>
<title id={`${idPrefix}-title`}>Arrow down</title>
<path d="M3 6l5 5 5-5" stroke={isDark ? '#5a5b66' : '#ccc'} strokeWidth={2} fill="none" strokeLinejoin="round" />
</svg>

View File

@ -0,0 +1,33 @@
import React from 'react'
import classnames from 'classnames'
type SvgCheckProps = {
disabled?: boolean
}
const SvgCheck: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement> & SvgCheckProps> = ({
disabled,
...props
}) => {
return (
<svg width={20} height={20} viewBox="0 0 20 20" fill="none" {...props}>
<path
className={classnames('svgCheck', { disabled })}
d="M3 9.857L8.09 15 17 6"
stroke="#3EA161"
strokeWidth={3}
strokeLinejoin="round"
/>
<style jsx>{`
.svgCheck {
stroke: #3ea161;
}
.svgCheck.disabled {
stroke: #9698a3;
}
`}</style>
</svg>
)
}
export { SvgCheck }

View File

@ -0,0 +1,65 @@
import React from 'react'
import classnames from 'classnames'
type SvgCheckboxProps = {
checked: boolean
disabled?: boolean
}
const SvgCheckbox: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement> & SvgCheckboxProps> = ({
checked,
disabled,
...props
}) => {
return (
<svg width={16} height={16} viewBox="0 0 16 16" fill="none" {...props}>
<path
className={classnames('unchecked', { disabled: disabled })}
d="M12.5.5h-9a3 3 0 00-3 3v9a3 3 0 003 3h9a3 3 0 003-3v-9a3 3 0 00-3-3z"
stroke="#CCC"
/>
<path
className={classnames('checkBg', { checked: checked })}
d="M13 0H3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V3a3 3 0 00-3-3z"
fill="#4C5ED5"
/>
<path
className={classnames('checkMark', { checked: checked })}
d="M4 7.143L6.91 10 12 5"
stroke="#fff"
strokeWidth={2}
strokeLinejoin="round"
/>
<style jsx>{`
.unchecked {
transition: all 100ms ease-in-out;
fill: #fafafa;
}
.unchecked.disabled {
fill: #ececec;
}
.checkBg {
transform-origin: center;
transition: all 100ms ease-in-out;
opacity: 0;
transform: scale(0);
}
.checkBg.checked {
opacity: 1;
transform: scale(1);
}
.checkMark {
transform-origin: center;
stroke-dasharray: 12;
stroke-dashoffset: 12;
transition: all 150ms 100ms ease-in-out;
}
.checkMark.checked {
stroke-dashoffset: 0;
}
`}</style>
</svg>
)
}
export { SvgCheckbox }

View File

@ -0,0 +1,34 @@
import React from 'react'
import classnames from 'classnames'
type SvgErrorProps = {
transparent?: boolean
}
const SvgError: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement> & SvgErrorProps> = ({
transparent,
...props
}) => {
return (
<svg width={20} height={20} viewBox="0 0 20 20" fill="none" {...props}>
<path
className={classnames('svgError', { transparent })}
d="M10 20c5.523 0 10-4.477 10-10S15.523 0 10 0 0 4.477 0 10s4.477 10 10 10z"
fill="#E15151"
/>
<path d="M14.95 6.464L13.536 5.05 5.05 13.536l1.414 1.414 8.486-8.486z" fill="#fff" />
<path d="M6.464 5.05L5.05 6.464l8.486 8.486 1.414-1.414L6.464 5.05z" fill="#fff" />
<style jsx>{`
.svgError {
fill: #e15151;
}
.svgError.transparent {
fill: #fff;
opacity: 0.2;
}
`}</style>
</svg>
)
}
export { SvgError }

View File

@ -4,40 +4,85 @@ import { uniqueId } from 'utils/ids'
const SvgFavycon: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (props) => {
const idPrefix = uniqueId('svg-')
return (
<svg width={64} height={64} aria-labelledby={`${idPrefix}-title`} {...props}>
<svg width={48} height={48} viewBox="0 0 48 48" aria-labelledby={`${idPrefix}-title`} {...props}>
<title id={`${idPrefix}-title`}>Favycon logo</title>
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="99.731%" id={`${idPrefix}-c`}>
<stop stopColor="#432EF5" offset="0%" />
<stop stopColor="#14BE8D" offset="100%" />
<linearGradient
id={`${idPrefix}-paint0_linear`}
x1={43.6252}
y1={9.02654}
x2={5.87483}
y2={37.4735}
gradientUnits="userSpaceOnUse">
<stop stopColor="#4B4D5A" />
<stop offset={1} stopColor="#191A1F" />
</linearGradient>
<linearGradient
id={`${idPrefix}-paint1_linear`}
x1={21.7849}
y1={12.1313}
x2={20.31}
y2={22.626}
gradientUnits="userSpaceOnUse">
<stop stopColor="#74EFB7" />
<stop offset={1} stopColor="#40DB81" />
</linearGradient>
<linearGradient
id={`${idPrefix}-paint2_linear`}
x1={29.417}
y1={22.2594}
x2={27.9421}
y2={32.7541}
gradientUnits="userSpaceOnUse">
<stop stopColor="#88DDE4" />
<stop offset={1} stopColor="#50B8C5" />
</linearGradient>
<linearGradient
id={`${idPrefix}-paint3_linear`}
x1={12}
y1={27}
x2={12}
y2={46.5}
gradientUnits="userSpaceOnUse">
<stop stopColor="#9C93F1" />
<stop offset={1} stopColor="#1A20FF" />
</linearGradient>
<filter x="-6.3%" y="-4.5%" width="112.5%" height="112.5%" filterUnits="objectBoundingBox" id={`${idPrefix}-a`}>
<feOffset dy={1} in="SourceAlpha" result="shadowOffsetOuter1" />
<feGaussianBlur stdDeviation={1} in="shadowOffsetOuter1" result="shadowBlurOuter1" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" in="shadowBlurOuter1" />
</filter>
<path
d="M29.669 4h4.662c6.766 0 10.83 1.021 14.414 2.938a19.99 19.99 0 018.317 8.317C58.979 18.84 60 22.903 60 29.669v4.662c0 6.766-1.021 10.83-2.938 14.414a19.99 19.99 0 01-8.317 8.317C45.16 58.979 41.097 60 34.331 60H29.67c-6.766 0-10.83-1.021-14.414-2.938a19.99 19.99 0 01-8.317-8.317C5.021 45.16 4 41.097 4 34.331V29.67c0-6.766 1.021-10.83 2.938-14.414a19.99 19.99 0 018.317-8.317C18.84 5.021 22.903 4 29.669 4z"
id={`${idPrefix}-b`}
/>
</defs>
<g fill="none" fillRule="evenodd">
<g transform="rotate(-8 32 32)">
<use fill="#000" filter={`url(${idPrefix}-a)`} xlinkHref={`${idPrefix}-b`} />
<use fill={`url(${idPrefix}-b)`} xlinkHref={`${idPrefix}-b`} />
</g>
<path
d="M25.795 4.597l4.617-.649c6.7-.942 10.865-.496 14.682.904a19.99 19.99 0 019.393 7.078c2.398 3.283 3.974 7.165 4.916 13.865l.649 4.617c.942 6.7.496 10.865-.904 14.682a19.99 19.99 0 01-7.078 9.393c-3.283 2.398-7.165 3.974-13.865 4.916l-4.617.649c-6.7.942-10.865.496-14.682-.904a19.99 19.99 0 01-9.393-7.078c-2.398-3.283-3.974-7.165-4.916-13.865l-.649-4.617c-.942-6.7-.496-10.865.904-14.682a19.99 19.99 0 017.078-9.393c3.283-2.398 7.165-3.974 13.865-4.916zm5.313 4.302l-4.618.65c-5.587.785-8.831 1.972-11.612 4.002a14.992 14.992 0 00-5.332 7.076C8.398 23.758 8.13 27.09 8.83 32.375l.07.517.65 4.618c.785 5.587 1.972 8.831 4.002 11.612a14.992 14.992 0 007.076 5.332c3.131 1.148 6.463 1.416 11.748.717l.517-.07 4.618-.65c5.587-.785 8.831-1.972 11.612-4.002a14.992 14.992 0 005.332-7.076c1.148-3.131 1.416-6.463.717-11.748l-.07-.517-.65-4.618c-.785-5.587-1.972-8.831-4.002-11.612a14.992 14.992 0 00-7.076-5.332C40.242 8.398 36.91 8.13 31.625 8.83l-.517.07z"
fillOpacity={0.48}
fill="#FFF"
fillRule="nonzero"
/>
<path
d="M24.396 41.02l-1.058-7.53 6.099-.857-.493-3.507-6.1.857-.565-4.023 7.6-1.068-.493-3.507-11 1.546 2.61 18.567 3.4-.477zm15.593 4.12l4.797-21.186-3.4.478-2.194 9.96-4.88-8.966-3.524.496 7.392 13.056-1.34 6.605 3.149-.442z"
fill="#FFF"
fillRule="nonzero"
/>
</g>
<path d="M8.861 2.073l37.066 5.152-5.288 37.202-37.066-5.152L8.861 2.073z" fill="#EEE" />
<path
d="M43.625 9.027L10.527 4.375 5.875 37.474l33.099 4.651 4.651-33.098z"
fill={`url(#${idPrefix}-paint0_linear)`}
stroke="#000"
strokeWidth={1.08696}
/>
<path
opacity={0.24}
d="M18.139 5.855l-.797-.112-4.548 32.293.797.112 4.548-32.293zM27.423 7.16l-.797-.112-4.549 32.293.797.111L27.423 7.16zM36.707 8.464l-.797-.112-4.549 32.293.797.112 4.549-32.293z"
fill="#fff"
/>
<path
opacity={0.24}
d="M42.37 15.014l-32.289-4.528-.115.818 32.29 4.528.115-.818zM41.066 24.298L8.776 19.77l-.115.817 32.29 4.529.115-.818zM39.761 33.582l-32.29-4.528-.115.817 32.29 4.528.115-.817z"
fill="#fff"
/>
<path
d="M19.642 11.83l4.286.602c1.08.152 1.455.32 1.82.586.365.267.631.62.787 1.044.155.425.213.832.06 1.912l-.601 4.285c-.152 1.08-.32 1.455-.586 1.82-.267.365-.62.631-1.044.787-.425.155-.832.213-1.911.061l-4.286-.602c-1.08-.152-1.455-.32-1.82-.586a2.223 2.223 0 01-.787-1.044c-.155-.424-.213-.832-.061-1.911l.602-4.285c.152-1.08.32-1.456.586-1.82.266-.366.62-.632 1.044-.788.424-.155.832-.213 1.911-.06z"
fill={`url(#${idPrefix}-paint1_linear)`}
/>
<path
d="M29.417 22.26a5.299 5.299 0 11-1.475 10.494 5.299 5.299 0 011.475-10.495z"
fill={`url(#${idPrefix}-paint2_linear)`}
/>
<path
d="M21.75 36.75c0-5.385-4.365-9.75-9.75-9.75s-9.75 4.365-9.75 9.75S6.615 46.5 12 46.5s9.75-4.365 9.75-9.75z"
fill={`url(#${idPrefix}-paint3_linear)`}
/>
<path
d="M12 27c5.385 0 9.75 4.365 9.75 9.75S17.385 46.5 12 46.5s-9.75-4.365-9.75-9.75S6.615 27 12 27zm0 1.5A8.25 8.25 0 1012 45a8.25 8.25 0 000-16.5z"
fill="#fff"
fillOpacity={0.32}
/>
<path fillRule="evenodd" clipRule="evenodd" d="M15 33v1.5h-3.75V36h3v1.5h-3v3h-1.5V33H15z" fill="#fff" />
</svg>
)
}

View File

@ -2,25 +2,21 @@ import React from 'react'
import useDarkMode from 'use-dark-mode'
import { uniqueId } from 'utils/ids'
const SvgImageUpload: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (props) => {
type SvgImageUploadProps = {
active?: boolean
}
const SvgImageUpload: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement> & SvgImageUploadProps> = ({
active = false,
...props
}) => {
const { value: isDark } = useDarkMode(false)
const idPrefix = uniqueId('svg-')
return (
<svg width={88} height={88} aria-labelledby={`${idPrefix}-title`} {...props}>
<svg width={88} height={88} viewBox="0 0 88 88" aria-labelledby={`${idPrefix}-title`} {...props}>
<title id={`${idPrefix}-title`}>Image Upload</title>
<path
d="M44 88c24.3 0 44-19.7 44-44S68.3 0 44 0 0 19.7 0 44s19.7 44 44 44z"
fill={isDark ? '#4B4D54' : '#EDF4F5'}
/>
<path
d="M64.133 64H23.867C21.73 64 20 62.238 20 60.064V27.936C20 25.762 21.731 24 23.867 24h40.266C66.27 24 68 25.762 68 27.936v32.128C68 62.238 66.269 64 64.133 64zM23.867 27.07a.86.86 0 00-.852.866v32.128c0 .23.09.45.25.613.16.162.376.254.602.254h40.266a.86.86 0 00.844-.867V27.936a.86.86 0 00-.844-.867H23.867z"
fill={isDark ? '#4D7E91' : '#95C7D6'}
/>
<path
d="M29.06 56.595h29.633a1.267 1.267 0 001.057-2.017l-4.087-5.55a1.268 1.268 0 00-1.913-.166l-4.2 3.998-9.75-13.237a1.268 1.268 0 00-2.085.06l-9.75 14.955a1.267 1.267 0 001.095 1.957z"
fill="#5AA656"
/>
<path d="M53.097 39.788a4.447 4.447 0 100-8.895 4.447 4.447 0 000 8.895z" fill="#FFBC4C" />
<path fill={active ? '#4c66d5' : isDark ? '#5A5B66' : '#CCC'} d="M43 20H45V68H43z" />
<path fill={active ? '#4c66d5' : isDark ? '#5A5B66' : '#CCC'} d="M20 43H68V45H20z" />
<style jsx>{`
path {
transition: fill 0.2s ease;

View File

@ -6,7 +6,7 @@ const SvgInfo: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>>
const { value: isDark } = useDarkMode(false)
const idPrefix = uniqueId('svg-')
return (
<svg width={20} height={20} aria-labelledby={`${idPrefix}-title`} {...props}>
<svg width={20} height={20} viewBox="0 0 20 20" aria-labelledby={`${idPrefix}-title`} {...props}>
<title id={`${idPrefix}-title`}>Info</title>
<path
d="M10 20c5.523 0 10-4.477 10-10S15.523 0 10 0 0 4.477 0 10s4.477 10 10 10z"

View File

@ -4,7 +4,7 @@ import { uniqueId } from 'utils/ids'
const SvgMoon: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (props) => {
const idPrefix = uniqueId('svg-')
return (
<svg width={32} height={32} aria-labelledby={`${idPrefix}-title`} {...props}>
<svg width={32} height={32} viewBox="0 0 32 32" aria-labelledby={`${idPrefix}-title`} {...props}>
<title id={`${idPrefix}-title`}>Activate dark mode</title>
<path
d="M24.613 22.62a9.999 9.999 0 11-8.287-16.602c.406-.03.79.189.975.552a1 1 0 01-.15 1.11 7.996 7.996 0 006.633 13.278 1 1 0 01.83 1.662z"

View File

@ -4,7 +4,7 @@ import { uniqueId } from 'utils/ids'
const SvgSun: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (props) => {
const idPrefix = uniqueId('svg-')
return (
<svg width={32} height={32} aria-labelledby={`${idPrefix}-title`} {...props}>
<svg width={32} height={32} viewBox="0 0 32 32" aria-labelledby={`${idPrefix}-title`} {...props}>
<title id={`${idPrefix}-title`}>Activate light mode</title>
<path
d="M23 17a7 7 0 11-14 0 7 7 0 0114 0zm-7 9a1 1 0 01.993.883L17 27v3a1 1 0 01-1.993.117L15 30v-3a1 1 0 011-1zm-6.422-2.578a1 1 0 01.13 1.255l-.078.103-2.12 2.12a1 1 0 11-1.5-1.324l.09-.086 2.12-2.13v.01a1 1 0 011.358.052zm14.099-.14l.103.078 2.12 2.12a1 1 0 01-1.307 1.487l-.103-.077-2.12-2.12a1 1 0 011.307-1.487zM6 16a1 1 0 01.117 1.993L6 18H3a1 1 0 01-.117-1.993L3 16h3zm23 0a1 1 0 01.117 1.993L29 18h-3a1 1 0 01-.117-1.993L26 16h3zM7.407 7.023l.103.077 2.13 2.12a1.004 1.004 0 01-1.325 1.503l-.095-.083L6.1 8.51a1 1 0 011.307-1.487zm18.44.13a1 1 0 01.13 1.254l-.077.103-2.12 2.13a1 1 0 01-.71.29 1 1 0 01-.799-1.61l.089-.1 2.13-2.12a1 1 0 011.358.052zM16 3a1 1 0 01.993.883L17 4v3a1 1 0 01-1.993.117L15 7V4a1 1 0 011-1z"

View File

@ -1,5 +1,9 @@
@import '../../styles/variables.scss';
.root {
font-family: Manrope, Arial, Helvetica Neue, Helvetica, sans-serif;
}
.h1 {
font-size: $font-size-h1;
line-height: $line-height-h1;
@ -45,6 +49,11 @@
line-height: $line-height-superscript;
}
.title {
font-size: $font-size-title;
line-height: $line-height-title;
}
.regular {
font-weight: 400;
}
@ -82,3 +91,16 @@
color: $black;
}
}
.green {
color: $green;
}
.muted {
color: $grey;
transition: color 0.2s ease;
&.dark {
color: $grey;
}
}

View File

@ -7,19 +7,22 @@ import styles from './index.module.scss'
type TypographyProps = {
children: PropTypes.ReactNodeLike
variant: 'h1' | 'smallBody' | 'regularBody' | 'mediumBody' | 'largeBody' | 'footer' | 'superscript'
variant: 'h1' | 'smallBody' | 'regularBody' | 'mediumBody' | 'largeBody' | 'footer' | 'superscript' | 'title'
weight: 'regular' | 'medium' | 'semiBold' | 'bold'
color: 'black' | 'gray' | 'white'
color: 'black' | 'gray' | 'white' | 'green'
tag?: string
muted?: boolean
} & React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
const Typography = ({ children, variant, weight, color, tag, ...props }: TypographyProps) => {
const Typography = ({ children, variant, weight, color, tag, muted, ...props }: TypographyProps) => {
const { value: isDark } = useDarkMode(false)
const className = classNames(
styles.root,
styles[variant],
styles[color],
styles[weight],
{ [styles.dark]: isDark },
{ [styles.muted]: muted },
props.className
)
const componentType = ['h1'].includes(variant) ? variant : 'p'

View File

@ -39,19 +39,24 @@
},
"dependencies": {
"@fiahfy/ico-convert": "0.0.7",
"@types/react-modal": "3.10.5",
"adm-zip": "0.4.14",
"classnames": "2.2.6",
"multer": "1.4.2",
"next": "9.3.5",
"nprogress": "0.2.0",
"object-fit-images": "3.2.4",
"preact": "10.4.0",
"preact-render-to-string": "5.1.6",
"preact-ssr-prepass": "1.0.1",
"prismjs": "1.20.0",
"prop-types": "15.7.2",
"react": "16.13.1",
"react-clipboard.js": "2.0.16",
"react-dom": "16.13.1",
"react-dropzone": "10.2.2",
"react-lazy-images": "1.1.0",
"react-modal": "3.11.2",
"sharp": "0.25.2",
"use-dark-mode": "2.3.1"
},
@ -73,7 +78,9 @@
"@types/classnames": "2.2.10",
"@types/multer": "1.4.2",
"@types/node": "13.11.1",
"@types/nprogress": "0.2.0",
"@types/object-fit-images": "3.2.0",
"@types/prismjs": "1.16.0",
"@types/react": "16.9.34",
"@types/sharp": "0.24.0",
"@typescript-eslint/eslint-plugin": "2.28.0",
@ -81,6 +88,7 @@
"all-contributors-cli": "6.14.1",
"babel-eslint": "10.1.0",
"babel-loader": "8.1.0",
"babel-plugin-prismjs": "2.0.1",
"babel-preset-react-app": "9.1.2",
"commitizen": "4.0.4",
"cz-conventional-changelog": "3.1.0",

View File

@ -4,7 +4,7 @@ import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render() {
return (
<Html>
<Html lang="en">
<Head />
<body>
<script src="/js/noflash.js" />

View File

@ -1,11 +1,12 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { NextApiResponse } from 'next'
import sharp from 'sharp'
import { convert } from '@fiahfy/ico-convert'
import multer from 'multer'
import AdmZip from 'adm-zip'
import { runMiddleware } from 'utils/api'
import { faviconSizesPrefixes, browserConfigTemplate, tutorialTemplate } from 'utils/favicon'
import { faviconSizes, pwaFaviconSizes, browserConfigTemplate, readmeTemplate, manifestCode } from 'utils/favicon'
import { MulterApiRequest, ACCEPT_MIME_TYPES } from 'utils/files'
const storage = multer.memoryStorage()
const upload = multer({ storage })
@ -16,37 +17,86 @@ export const config = {
},
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
export default async (req: MulterApiRequest, res: NextApiResponse) => {
switch (req.method) {
case 'POST':
await runMiddleware(req, res, upload.single('icon'))
const zip = new AdmZip()
// add folder
zip.addFile('icons/', Buffer.alloc(0))
const file: Buffer = (req as any).file.buffer
if (!req.file) {
return res.status(400).end('The icon field is required')
}
if (!Object.keys(ACCEPT_MIME_TYPES).includes(req.file.mimetype)) {
return res.status(422).end(`The icon field should contain a ${Object.values(ACCEPT_MIME_TYPES).join(' or ')}`)
}
Object.keys(faviconSizesPrefixes).map(async (size) => {
const [width, height] = size.split('x')
const scaledPng = await sharp(file)
.resize(parseInt(width), parseInt(height), { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toBuffer()
// @ts-ignore
faviconSizesPrefixes[size].forEach((prefix) => {
zip.addFile(`icons/${prefix}-${size}.png`, scaledPng)
const fileBuffer = req.file.buffer
const { format, width, height } = await sharp(fileBuffer).metadata()
const isSvg = format === 'svg'
const pwaCompatible = req.body.pwa ? req.body.pwa === '1' : false
if (!isSvg && !pwaCompatible && ((width && width < 310) || (height && height < 310))) {
return res.status(422).end('The icon should be at least 310px')
}
if (!isSvg && pwaCompatible && ((width && width < 512) || (height && height < 512))) {
return res.status(422).end('The icon should be at least 512px for PWA compatibility')
}
const zip = new AdmZip()
// add icons folder
zip.addFile('icons/', Buffer.alloc(0))
const sizes = pwaCompatible ? [...faviconSizes, ...pwaFaviconSizes] : faviconSizes
// PNG favicon files
const promises = sizes.map((size) => {
return new Promise((resolve) => {
const iconWidth = parseInt(size.split('x')[0])
const iconHeight = parseInt(size.split('x')[1])
sharp(fileBuffer, { density: isSvg && width ? (72 * iconWidth) / width : 72 })
.resize(iconWidth, iconHeight, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toBuffer()
.then((scaledPng) => {
zip.addFile(`icons/favicon-${size}.png`, scaledPng)
resolve()
})
})
})
const scaledPng = await sharp(file)
.resize(256, 256, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toBuffer()
const ico = await convert(scaledPng)
zip.addFile('icons/favicon.ico', ico)
// ICO favicon file
const icoPromise = new Promise((resolve) => {
sharp(fileBuffer, { density: isSvg && width ? (72 * 256) / width : 72 })
.resize(256, 256, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toBuffer()
.then(async (scaledPng) => {
const ico = await convert(scaledPng)
zip.addFile('icons/favicon.ico', ico)
resolve()
})
})
zip.addFile('icons/browserconfig.xml', Buffer.alloc(browserConfigTemplate.length, browserConfigTemplate))
zip.addFile('readme.txt', Buffer.alloc(tutorialTemplate.length, tutorialTemplate))
promises.push(icoPromise)
await Promise.all(promises)
// SVG file
if (isSvg) {
zip.addFile('icons/favicon.svg', fileBuffer)
}
// PWA manifest file
if (pwaCompatible) {
const manifest = manifestCode()
zip.addFile('icons/manifest.json', Buffer.alloc(manifest.length, manifest))
}
// IE10+ browser config file
const browserConfig = browserConfigTemplate()
zip.addFile('icons/browserconfig.xml', Buffer.alloc(browserConfig.length, browserConfig))
// Readme file
const readme = readmeTemplate(undefined, isSvg, pwaCompatible)
zip.addFile('readme.txt', Buffer.alloc(readme.length, readme))
res.setHeader('Content-disposition', 'attachment; filename=favicons.zip')
res.setHeader('Content-type', 'application/zip')

View File

@ -10,48 +10,3 @@
padding: 52px 20px;
margin: 0 auto;
}
.info {
width: 100%;
max-width: 350px;
}
.firstParagraph {
margin-top: 8px;
}
.secondParagraph {
margin-top: 16px;
margin-bottom: 60px;
}
.dragAndDrop {
position: relative;
display: flex;
align-items: flex-start;
justify-content: center;
width: 100%;
max-width: 540px;
padding: 41px 0 0;
}
.background {
position: absolute;
top: 10px;
right: 0;
left: 0;
z-index: -1;
width: 100%;
overflow: hidden;
border-radius: 8px;
transform: rotate(2deg);
}
.image {
position: absolute;
top: 57px;
right: 15px;
width: 184px;
height: 108px;
transform: translate(100%, -100%);
}

View File

@ -1,76 +1,60 @@
import React, { useMemo } from 'react'
import React, { useMemo, useState, useEffect } from 'react'
import { NextPage } from 'next'
import useDarkMode from 'use-dark-mode'
import { LazyImage } from 'components/lazy-image'
import NProgress from 'nprogress'
import { BaseLayout } from 'components/base-layout'
import { Typography } from 'components/typography'
import { SEO } from 'components/seo'
import { ToolTitle } from 'components/tool-title'
import { FavyconWizard } from 'components/favycon-wizard'
import { FavyconInfo } from 'components/favycon-info'
import { DragAndDrop } from 'components/drag-and-drop'
import { SEO } from 'components/seo'
import styles from './index.module.scss'
NProgress.configure({ minimum: 0.15, speed: 300, trickleSpeed: 150, showSpinner: false })
const getRandomNumber = (min = 1, max = 3) => Math.floor(Math.random() * max) + min
const unsplashImagesUrls = [
'https://unsplash.com/photos/zKnQnyARggY',
'https://unsplash.com/photos/UQAQm_EpWR8',
'https://unsplash.com/photos/IXUM4cJynP0',
]
const Home: NextPage = () => {
const { value: isDark } = useDarkMode(false)
const backgroundId = useMemo(() => getRandomNumber(), [])
const onDrop = (acceptedFiles: File[]) => {
console.log('acceptedFiles[0]', acceptedFiles[0])
const [error, setError] = useState('')
const [file, setFile] = useState(false)
const [counter, setCounter] = useState(0)
useEffect(() => {
if (!file) {
setCounter((c) => c + 1)
}
}, [file])
const onGenerate = async (file: File, pwa: boolean) => {
try {
NProgress.start()
const formData = new FormData()
formData.append('icon', file)
formData.append('pwa', pwa ? '1' : '0')
const response = await fetch('/api/favycon', {
method: 'POST',
body: formData,
})
return await response.arrayBuffer()
} catch (error) {
throw error
} finally {
NProgress.done()
}
}
return (
<BaseLayout>
<SEO
title="Favycon"
title="Favycon - A favicon generator tool"
description="A small online tool to help you generate your favicon in all the sizes and formats you need."
/>
<main className={styles.container}>
<div className={styles.info}>
<ToolTitle>Favycon</ToolTitle>
<Typography variant="largeBody" weight="medium" className={styles.firstParagraph}>
A small online tool to help you generate your favicon in all the sizes and formats you need.
</Typography>
<Typography variant="largeBody" weight="medium" className={styles.secondParagraph}>
Just drag &amp; drop an image and you will then get a downloadable file alongside some documentation on how
to add the favicons.
</Typography>
<hr />
<Typography variant="footer" weight="semiBold" color="gray">
Created by <a href="/#">4 people</a> on their 2020s worldwide quarantine. Background image from{' '}
<a href={unsplashImagesUrls[backgroundId - 1]} target="_blank" rel="noopener noreferrer">
Unsplash
</a>
.
</Typography>
</div>
<div className={styles.dragAndDrop}>
<div className={styles.background}>
<LazyImage
src={`/images/unsplash-${backgroundId}@1x.jpg`}
srcRetina={`/images/unsplash-${backgroundId}@2x.jpg`}
srcPlaceholder={`/images/unsplash-${backgroundId}@placeholder.jpg`}
alt="Unsplash"
aspectRatio="540/354"
/>
</div>
<DragAndDrop onDrop={onDrop} />
<div className={styles.image}>
<LazyImage
src={`/images/dnd-${isDark ? 'dark' : 'light'}@1x.png`}
srcRetina={`/images/dnd-${isDark ? 'dark' : 'light'}@2x.png`}
srcPlaceholder={`/images/dnd-${isDark ? 'dark' : 'light'}@1x.png`}
alt="Drag and drop here!"
aspectRatio="184/108"
/>
</div>
</div>
<FavyconInfo imageIndex={backgroundId - 1} />
<FavyconWizard backgroundId={backgroundId} error={error} clearError={() => setError('')} showDndImage={!file}>
<DragAndDrop key={counter} onFile={setFile} onGenerate={onGenerate} onError={setError} />
</FavyconWizard>
</main>
</BaseLayout>
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,11 +1,33 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.7946 4.59695L30.4117 3.94804C37.1118 3.00641 41.2773 3.45211 45.0944 4.85186C48.9116 6.25162 52.0895 8.64632 54.4871 11.9298C56.8848 15.2132 58.4614 19.0945 59.403 25.7946L60.052 30.4117C60.9936 37.1118 60.5479 41.2773 59.1481 45.0944C57.7484 48.9116 55.3537 52.0895 52.0702 54.4871C48.7868 56.8848 44.9055 58.4614 38.2054 59.4031L33.5882 60.052C26.8882 60.9936 22.7227 60.5479 18.9055 59.1481C15.0884 57.7484 11.9105 55.3537 9.51285 52.0702C7.11517 48.7868 5.53857 44.9055 4.59694 38.2054L3.94804 33.5883C3.00641 26.8882 3.4521 22.7227 4.85185 18.9056C6.25161 15.0884 8.64632 11.9105 11.9298 9.51285C15.2132 7.11517 19.0945 5.53858 25.7946 4.59695Z" fill="url(#paint0_linear)"/>
<path d="M25.7946 4.59695L30.4117 3.94804C37.1118 3.00641 41.2773 3.45211 45.0944 4.85186C48.9116 6.25162 52.0895 8.64632 54.4871 11.9298C56.8848 15.2132 58.4614 19.0945 59.403 25.7946L60.052 30.4117C60.9936 37.1118 60.5479 41.2773 59.1481 45.0944C57.7484 48.9116 55.3537 52.0895 52.0702 54.4871C48.7868 56.8848 44.9055 58.4614 38.2054 59.4031L33.5882 60.052C26.8882 60.9936 22.7227 60.5479 18.9055 59.1481C15.0884 57.7484 11.9105 55.3537 9.51285 52.0702C7.11517 48.7868 5.53857 44.9055 4.59694 38.2054L3.94804 33.5883C3.00641 26.8882 3.4521 22.7227 4.85185 18.9056C6.25161 15.0884 8.64632 11.9105 11.9298 9.51285C15.2132 7.11517 19.0945 5.53858 25.7946 4.59695ZM31.1076 8.89938L26.4904 9.54829C20.9033 10.3335 17.6588 11.5205 14.8784 13.5508C12.393 15.3658 10.6057 17.7376 9.54618 20.627C8.39792 23.7583 8.13049 27.0901 8.82878 32.3748L8.89938 32.8924L9.54828 37.5096C10.3335 43.0967 11.5205 46.3412 13.5508 49.1216C15.3658 51.607 17.7375 53.3943 20.627 54.4538C23.7583 55.6021 27.0901 55.8695 32.3748 55.1712L32.8924 55.1006L37.5096 54.4517C43.0967 53.6665 46.3412 52.4795 49.1216 50.4492C51.607 48.6342 53.3942 46.2624 54.4538 43.373C55.6021 40.2417 55.8695 36.9099 55.1712 31.6252L55.1006 31.1076L54.4517 26.4904C53.6665 20.9033 52.4795 17.6588 50.4492 14.8784C48.6342 12.393 46.2624 10.6057 43.373 9.54619C40.2417 8.39793 36.9099 8.1305 31.6252 8.82879L31.1076 8.89938Z" fill="white" fill-opacity="0.48"/>
<path d="M24.3959 41.0207L23.3376 33.4905L29.4369 32.6333L28.944 29.1261L22.8447 29.9833L22.2793 25.9603L29.8785 24.8923L29.3855 21.3851L18.3868 22.9309L20.9963 41.4984L24.3959 41.0207ZM39.989 45.1406L44.7862 23.9543L41.3866 24.4321L39.1921 34.3917L34.3124 25.4263L30.7878 25.9216L38.1798 38.9783L36.8394 45.5833L39.989 45.1406Z" fill="white"/>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.86133 2.07298L45.927 7.22493L40.6387 44.427L3.57299 39.2751L8.86133 2.07298Z" fill="#EEEEEE"/>
<path d="M43.6252 9.02654L10.5266 4.37483L5.87484 37.4735L38.9735 42.1252L43.6252 9.02654Z" fill="url(#paint0_linear)" stroke="black" stroke-width="1.08696"/>
<path opacity="0.24" d="M18.139 5.85465L17.3419 5.74286L12.7935 38.0359L13.5906 38.1477L18.139 5.85465Z" fill="white"/>
<path opacity="0.24" d="M27.4228 7.15941L26.6257 7.04762L22.0773 39.3406L22.8744 39.4524L27.4228 7.15941Z" fill="white"/>
<path opacity="0.24" d="M36.7066 8.46414L35.9095 8.35236L31.361 40.6454L32.1581 40.7572L36.7066 8.46414Z" fill="white"/>
<path opacity="0.24" d="M42.3706 15.0142L10.081 10.4862L9.96584 11.3037L42.2555 15.8317L42.3706 15.0142Z" fill="white"/>
<path opacity="0.24" d="M41.0659 24.298L8.77625 19.77L8.66109 20.5875L40.9507 25.1155L41.0659 24.298Z" fill="white"/>
<path opacity="0.24" d="M39.7612 33.5817L7.4715 29.0537L7.35634 29.8713L39.646 34.3993L39.7612 33.5817Z" fill="white"/>
<path d="M19.6422 11.8302L23.9276 12.4324C25.0071 12.5842 25.3828 12.7516 25.7478 13.0181C26.1128 13.2847 26.379 13.6379 26.5347 14.0623C26.6903 14.4866 26.7476 14.8939 26.5959 15.9735L25.9937 20.2588C25.8419 21.3384 25.6745 21.714 25.408 22.079C25.1414 22.4441 24.7882 22.7103 24.3638 22.8659C23.9395 23.0215 23.5322 23.0789 22.4526 22.9271L18.1673 22.3249C17.0877 22.1731 16.7121 22.0057 16.3471 21.7392C15.9821 21.4726 15.7158 21.1194 15.5602 20.695C15.4046 20.2707 15.3472 19.8634 15.499 18.7839L16.1012 14.4985C16.253 13.419 16.4204 13.0433 16.6869 12.6783C16.9535 12.3133 17.3067 12.0471 17.7311 11.8914C18.1554 11.7358 18.5627 11.6785 19.6422 11.8302Z" fill="url(#paint1_linear)"/>
<path d="M29.417 22.2594C32.315 22.6667 34.3342 25.3462 33.9269 28.2442C33.5196 31.1423 30.8401 33.1614 27.9421 32.7541C25.044 32.3468 23.0249 29.6673 23.4322 26.7693C23.8395 23.8713 26.519 21.8521 29.417 22.2594Z" fill="url(#paint2_linear)"/>
<path d="M21.75 36.75C21.75 31.3652 17.3848 27 12 27C6.61522 27 2.25 31.3652 2.25 36.75C2.25 42.1348 6.61522 46.5 12 46.5C17.3848 46.5 21.75 42.1348 21.75 36.75Z" fill="url(#paint3_linear)"/>
<path d="M12 27C17.3848 27 21.75 31.3652 21.75 36.75C21.75 42.1348 17.3848 46.5 12 46.5C6.61522 46.5 2.25 42.1348 2.25 36.75C2.25 31.3652 6.61522 27 12 27ZM12 28.5C7.44365 28.5 3.75 32.1937 3.75 36.75C3.75 41.3063 7.44365 45 12 45C16.5563 45 20.25 41.3063 20.25 36.75C20.25 32.1937 16.5563 28.5 12 28.5Z" fill="white" fill-opacity="0.32"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 33V34.5H11.25V36H14.25V37.5H11.25V40.5H9.75V33H15Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear" x1="28.1031" y1="4.2725" x2="35.8759" y2="59.5782" gradientUnits="userSpaceOnUse">
<stop stop-color="#432EF5"/>
<stop offset="1" stop-color="#14BE8D"/>
<linearGradient id="paint0_linear" x1="43.6252" y1="9.02654" x2="5.87483" y2="37.4735" gradientUnits="userSpaceOnUse">
<stop stop-color="#4B4D5A"/>
<stop offset="1" stop-color="#191A1F"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="21.7849" y1="12.1313" x2="20.31" y2="22.626" gradientUnits="userSpaceOnUse">
<stop stop-color="#74EFB7"/>
<stop offset="1" stop-color="#40DB81"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="29.417" y1="22.2594" x2="27.9421" y2="32.7541" gradientUnits="userSpaceOnUse">
<stop stop-color="#88DDE4"/>
<stop offset="1" stop-color="#50B8C5"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="12" y1="27" x2="12" y2="46.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#9C93F1"/>
<stop offset="1" stop-color="#1A20FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -32,3 +32,11 @@
font-weight: 700;
font-display: swap;
}
@font-face {
font-family: 'Monaco';
src: url('/fonts/Monaco/Monaco.ttf');
font-style: normal;
font-weight: 400;
font-display: swap;
}

View File

@ -1,6 +1,8 @@
@import 'reset';
@import 'font';
@import 'variables';
@import 'nprogress';
@import 'prism';
html,
body {
@ -22,7 +24,7 @@ body {
}
&.dark-mode {
background: $dark-black;
background: $darker-black;
}
}
@ -43,3 +45,16 @@ hr {
background: $darker-grey;
}
}
.ReactModal__Overlay {
opacity: 0;
transition: opacity 0.2s ease;
}
.ReactModal__Overlay--after-open {
opacity: 1;
}
.ReactModal__Overlay--before-close {
opacity: 0;
}

65
styles/nprogress.scss Normal file
View File

@ -0,0 +1,65 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
position: fixed;
top: 0;
left: 0;
z-index: 1031;
width: 100%;
height: 2px;
background: $link;
}
/* Fancy blur effect */
#nprogress .peg {
position: absolute;
right: 0;
display: block;
width: 100px;
height: 100%;
box-shadow: 0 0 10px $link, 0 0 5px $link;
opacity: 1;
transform: rotate(3deg) translate(0, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
position: fixed;
top: 15px;
right: 15px;
z-index: 1031;
display: block;
}
#nprogress .spinner-icon {
box-sizing: border-box;
width: 18px;
height: 18px;
border: solid 2px transparent;
border-top-color: $link;
border-left-color: $link;
border-radius: 50%;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
position: relative;
overflow: hidden;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

122
styles/prism.scss Normal file
View File

@ -0,0 +1,122 @@
/* stylelint-disable */
/* PrismJS 1.20.0
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup */
/**
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
* Based on https://github.com/chriskempson/tomorrow-theme
* @author Rose Pritchard
*/
code[class*='language-'],
pre[class*='language-'] {
color: #ccc;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: #2d2d2d;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #999;
}
.token.punctuation {
color: #ccc;
}
.token.tag,
.token.attr-name,
.token.namespace,
.token.deleted {
color: #e2777a;
}
.token.function-name {
color: #6196cc;
}
.token.boolean,
.token.number,
.token.function {
color: #f08d49;
}
.token.property,
.token.class-name,
.token.constant,
.token.symbol {
color: #f8c555;
}
.token.selector,
.token.important,
.token.atrule,
.token.keyword,
.token.builtin {
color: #cc99cd;
}
.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.variable {
color: #7ec699;
}
.token.operator,
.token.entity,
.token.url {
color: #67cdcc;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.inserted {
color: green;
}

View File

@ -7,11 +7,15 @@ $dark-grey: #3d3f47;
$darker-grey: #33353d;
$black: #000;
$dark-black: #191a1f;
$dark-black: #222329;
$darker-black: #101114;
$link: #4c66d5;
$link-dark: #7083ff;
$red: #e15151;
$green: #3ea161;
$font-size-h1: 52px;
$line-height-h1: 64px;
@ -32,3 +36,6 @@ $line-height-footer: 24px;
$font-size-superscript: 12px;
$line-height-superscript: 24px;
$font-size-title: 15px;
$line-height-title: 24px;

View File

@ -1,62 +1,131 @@
export const faviconSizesPrefixes = {
'57x57': ['apple-icon'],
'60x60': ['apple-icon'],
'72x72': ['apple-icon'],
'76x76': ['apple-icon'],
'114x114': ['apple-icon'],
'120x120': ['apple-icon'],
'144x144': ['apple-icon', 'ms-icon'],
'152x152': ['apple-icon'],
'180x180': ['apple-icon'],
'16x16': ['favicon'],
'32x32': ['favicon'],
'96x96': ['favicon'],
'192x192': ['favicon'],
'70x70': ['ms-icon'],
'150x150': ['ms-icon'],
'310x150': ['ms-icon'],
'310x310': ['ms-icon'],
}
export const faviconSizes = [
'57x57',
'60x60',
'72x72',
'76x76',
'114x114',
'120x120',
'144x144',
'152x152',
'180x180',
'16x16',
'32x32',
'96x96',
'192x192',
'70x70',
'150x150',
'310x310',
]
export const browserConfigTemplate = `
export const pwaFaviconSizes = ['128x128', '384x384', '512x512']
export const browserConfigTemplate = (color = '#ffffff') => `
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/ms-icon-70x70.png"/>
<square150x150logo src="/ms-icon-150x150.png"/>
<wide310x150logo src="/ms-icon-310x150.png"/>
<square310x310logo src="/ms-icon-310x310.png"/>
<TileColor>#ffffff</TileColor>
<square70x70logo src="/favicon-70x70.png"/>
<square150x150logo src="/favicon-150x150.png"/>
<square310x310logo src="/favicon-310x310.png"/>
<TileColor>${color}</TileColor>
</tile>
</msapplication>
</browserconfig>
`
export const tutorialTemplate = `
Thank you for using Favycon!
Now that you have all favicon files generator, you can copy all files
from the icons folder into your project public folder.
Last but not least add the following HTML tags to your index.html file.
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
export const headTemplate = (
color = '#ffffff',
isSvg = false,
isPwa = false
) => `<link rel="apple-touch-icon" sizes="57x57" href="/favicon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180x180.png">${
isSvg ? '\n<link rel="icon" type="image/svg+xml" href="/favicon.svg">' : ''
}
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<meta name="msapplication-config" content="/browserconfig.xml">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
`
<meta name="msapplication-TileColor" content="${color}">
<meta name="msapplication-TileImage" content="/favicon-144x144.png">
<meta name="msapplication-config" content="/browserconfig.xml">${
isPwa ? `\n<link rel="manifest" href="/manifest.json">\n<meta name="theme-color" content="${color}">` : ''
}`
export const readmeTemplate = (color = '#ffffff', isSvg = false, isPwa = false) => `Thank you for using Favycon!
Now that you have all favicon files generator, you can copy all files
from the icons folder into your project public folder.
Last but not least add the following HTML tags to your index.html file.
${headTemplate(color, isSvg, isPwa)}`
export const manifestCode = (color = '#ffffff') => `{
"name": "My App",
"description": "My App description",
"short_name": "My App",
"icons": [
{
"src": "/favicon-72x72.png",
"type": "image/png",
"sizes": "72x72",
"purpose": "any maskable"
},
{
"src": "/favicon-96x96.png",
"type": "image/png",
"sizes": "96x96",
"purpose": "any maskable"
},
{
"src": "/favicon-128x128.png",
"type": "image/png",
"sizes": "128x128",
"purpose": "any maskable"
},
{
"src": "/favicon-144x144.png",
"type": "image/png",
"sizes": "144x144",
"purpose": "any maskable"
},
{
"src": "/favicon-152x152.png",
"type": "image/png",
"sizes": "152x152",
"purpose": "any maskable"
},
{
"src": "/favicon-192x192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "any maskable"
},
{
"src": "/favicon-384x384.png",
"type": "image/png",
"sizes": "384x384",
"purpose": "any maskable"
},
{
"src": "/favicon-512x512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "any maskable"
}
],
"scope": "/",
"start_url": "/?source=pwa",
"display": "standalone",
"theme_color": "${color}",
"background_color": "${color}"
}`

55
utils/files.ts Normal file
View File

@ -0,0 +1,55 @@
import { NextApiRequest } from 'next'
export const ONE_MB = 1048576 // 1MB
export const MIN_SIZE = 310
export const MIN_PWA_SIZE = 512
export const SVG_MIME_TYPE = 'image/svg+xml'
export const RECOMMENDED_MIME_TYPES = ['image/png', SVG_MIME_TYPE]
export const ACCEPT_MIME_TYPES = {
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/svg+xml': 'SVG',
} as { [key: string]: string }
export const getImageFileSizes = (file: File) =>
new Promise<{ width: number; height: number }>((resolve) => {
const fileReader = new FileReader()
fileReader.onload = () => {
const img = new Image()
img.onload = () => {
resolve({ width: img.width, height: img.height })
}
img.src = fileReader.result as string
}
fileReader.readAsDataURL(file)
})
export const downloadFile = (data: ArrayBuffer, filename: string) => {
if ('msSaveOrOpenBlob' in window.navigator) {
// Blob for IE11
window.navigator.msSaveOrOpenBlob(new Blob([data]), filename)
} else {
// Blob navigator
const url = window.URL.createObjectURL(new Blob([data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', filename)
document.body.appendChild(link)
link.click()
link.remove()
}
}
export type MulterApiRequest = NextApiRequest & {
file: {
fieldname: string
originalname: string
encoding: string
mimetype: string
size: number
destination: string
filename: string
path: string
buffer: Buffer
}
}

View File

@ -2365,6 +2365,11 @@
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
integrity sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ==
"@types/clipboard@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/clipboard/-/clipboard-2.0.1.tgz#75a74086c293d75b12bc93ff13bc7797fef05a40"
integrity sha512-gJJX9Jjdt3bIAePQRRjYWG20dIhAgEqonguyHxXuqALxsoDsDLimihqrSg8fXgVTJ4KZCzkfglKtwsh/8dLfbA==
"@types/color-name@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
@ -2452,6 +2457,11 @@
resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.2.tgz#d070fe6a6b78755d1092a3dc492d34c3d8f871c4"
integrity sha512-4QQmOF5KlwfxJ5IGXFIudkeLCdMABz03RcUXu+LCb24zmln8QW6aDjuGl4d4XPVLf2j+FnjelHTP7dvceAFbhA==
"@types/nprogress@0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f"
integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A==
"@types/object-fit-images@3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@types/object-fit-images/-/object-fit-images-3.2.0.tgz#ffa0b3243ade3b89a2b12a08e92dbbea75b825f4"
@ -2462,6 +2472,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/prismjs@1.16.0":
version "1.16.0"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.16.0.tgz#4328c9f65698e59f4feade8f4e5d928c748fd643"
integrity sha512-mEyuziLrfDCQ4juQP1k706BUU/c8OGn/ZFl69AXXY6dStHClKX4P+N8+rhqpul1vRDA2VOygzMRSJJZHyDEOfw==
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
@ -2497,6 +2512,13 @@
dependencies:
"@types/react" "*"
"@types/react-modal@3.10.5":
version "3.10.5"
resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.10.5.tgz#5aa40bcb71b59243126069330856eb430ed3adcc"
integrity sha512-iLL9afYbcgYlboW2J8mFNKH2tFgErIHR0q+JgHotKrFK99+d97v+V/dxL5LVoxt1OjlnWsuwj8sZuBMVsI1MtQ==
dependencies:
"@types/react" "*"
"@types/react-syntax-highlighter@11.0.4":
version "11.0.4"
resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz#d86d17697db62f98046874f62fdb3e53a0bbc4cd"
@ -3521,6 +3543,11 @@ babel-plugin-named-asset-import@^0.3.1:
resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz#c9750a1b38d85112c9e166bf3ef7c5dbc605f4be"
integrity sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA==
babel-plugin-prismjs@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/babel-plugin-prismjs/-/babel-plugin-prismjs-2.0.1.tgz#b56095f423926662259de8f5ee50a7afbcf0fd92"
integrity sha512-GqQGa3xX3Z2ft97oDbGvEFoxD8nKqb3ZVszrOc5H7icnEUA56BIjVYm86hfZZA82uuHLwTIfCXbEKzKG1BzKzg==
babel-plugin-react-docgen@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-4.1.0.tgz#1dfa447dac9ca32d625a123df5733a9e47287c26"
@ -6315,6 +6342,11 @@ execall@^2.0.0:
dependencies:
clone-regexp "^2.1.0"
exenv@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@ -10323,6 +10355,11 @@ npmlog@^4.0.1, npmlog@^4.1.2, npmlog@~4.1.2:
gauge "~2.7.3"
set-blocking "~2.0.0"
nprogress@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
integrity sha1-y480xTIT2JVyP8urkH6UIq28r7E=
nth-check@^1.0.2, nth-check@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
@ -11528,7 +11565,7 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
prismjs@^1.8.4:
prismjs@1.20.0, prismjs@^1.8.4:
version "1.20.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03"
integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==
@ -11611,7 +11648,7 @@ prop-types-exact@1.2.0:
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@15.7.2, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -11846,6 +11883,15 @@ react-clientside-effect@^1.2.2:
dependencies:
"@babel/runtime" "^7.0.0"
react-clipboard.js@2.0.16:
version "2.0.16"
resolved "https://registry.yarnpkg.com/react-clipboard.js/-/react-clipboard.js-2.0.16.tgz#45a60684dd6a36ff97e40c53069a27e97a4b6c9c"
integrity sha512-COwmnbrRbl8y4f/SjtonnJTeBRD03YzsHBL5on8iL/uyjERsMkKC7djtfmns7iRAbzadn/84MdpaqaQ3ITP47g==
dependencies:
"@types/clipboard" "^2.0.1"
clipboard "^2.0.0"
prop-types "^15.5.0"
react-color@^2.17.0:
version "2.18.0"
resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.18.0.tgz#34956f0bac394f6c3bc01692fd695644cc775ffd"
@ -12012,11 +12058,21 @@ react-lazy-images@1.1.0:
react-intersection-observer "^6.1.0"
unionize "^2.1.2"
react-lifecycles-compat@^3.0.4:
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-modal@3.11.2:
version "3.11.2"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4"
integrity sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w==
dependencies:
exenv "^1.2.0"
prop-types "^15.5.10"
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-popper-tooltip@^2.8.3:
version "2.11.1"
resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-2.11.1.tgz#3c4bdfd8bc10d1c2b9a162e859bab8958f5b2644"