diff --git a/.all-contributorsrc b/.all-contributorsrc
index 06f0dbb..6c01068 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -51,6 +51,16 @@
"code",
"review"
]
+ },
+ {
+ "login": "miguellteixeira",
+ "name": "Miguel Teixeira",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/17954325?v=4",
+ "profile": "https://github.com/miguellteixeira",
+ "contributions": [
+ "code",
+ "review"
+ ]
}
],
"contributorsPerLine": 7
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..8d31df2
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,3 @@
+node_modules
+.next
+storybook-static
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
index 4a62ec9..c752bfb 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -2,14 +2,14 @@
"parser": "@typescript-eslint/parser",
"extends": [
"react-app",
- "prettier",
+ "plugin:prettier/recommended",
"prettier/react",
"prettier/@typescript-eslint",
"plugin:react/recommended",
+ "plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
- "plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"],
"parserOptions": {
"project": "./tsconfig.json"
},
@@ -17,12 +17,26 @@
"prettier/prettier": "error",
"react/prop-types": "off",
"no-param-reassign": "off",
- "react/jsx-filename-extension": [1, { "extensions": [".js", ".ts", "tsx"] }],
+ "react/jsx-filename-extension": [
+ 1,
+ {
+ "extensions": [".js", ".ts", ".tsx"]
+ }
+ ],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/no-explicit-any": "off",
- "@typescript-eslint/ban-ts-ignore": "off"
+ "@typescript-eslint/ban-ts-ignore": "off",
+ "@typescript-eslint/ban-ts-comment": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/no-unsafe-call": "off",
+ "@typescript-eslint/no-extra-semi": "off",
+ "@typescript-eslint/no-unsafe-assignment": "off",
+ "@typescript-eslint/no-unsafe-return": "off",
+ "@typescript-eslint/no-floating-promises": "off",
+ "@typescript-eslint/no-unsafe-member-access": "off",
+ "@typescript-eslint/unbound-method": "off"
},
"overrides": [
{
diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml
index 1f2b70f..af71e31 100644
--- a/.github/workflows/commitlint.yml
+++ b/.github/workflows/commitlint.yml
@@ -4,6 +4,7 @@ on: pull_request
jobs:
lint:
+ name: Lint commit messages
runs-on: ubuntu-latest
steps:
- name: Checkout
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index b534d30..3cbae9e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -4,6 +4,7 @@ on: pull_request
jobs:
code:
+ name: Lint code
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -11,8 +12,10 @@ jobs:
- name: Use Node.js 12
uses: actions/setup-node@v1
with:
- node-version: 12.x
+ node-version: "12"
- name: Install dependencies
- run: yarn install --frozen-lockfile &> /dev/null
- - name: Lint
- run: yarn lint
+ uses: bahmutov/npm-install@v1
+ - name: Lint Scripts
+ run: yarn lint:scripts
+ - name: Lint CSS
+ run: yarn lint:styles
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index eec7b56..8ca4281 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,6 +7,7 @@ on:
jobs:
release:
+ name: Semantic Release
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -14,9 +15,9 @@ jobs:
- name: Use Node.js 12
uses: actions/setup-node@v1
with:
- node-version: 12.x
+ node-version: "12"
- name: Install dependencies
- run: yarn install --frozen-lockfile &> /dev/null
+ uses: bahmutov/npm-install@v1
- name: Build static assets
run: yarn build
- name: Semantic Release
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..b3f88bc
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,45 @@
+name: Tests
+
+on: pull_request
+
+jobs:
+ wait_deploy:
+ name: Wait Vercel Deploy
+ runs-on: ubuntu-latest
+ outputs:
+ preview_url: ${{ steps.waitForVercelPreviewDeployment.outputs.url }}
+ steps:
+ - name: Wait for Vercel preview deployment to be ready
+ uses: patrickedqvist/wait-for-vercel-preview@master
+ id: waitForVercelPreviewDeployment
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ max_timeout: 120
+
+ test_e2e:
+ name: Run Cypress
+ needs: wait_deploy
+ runs-on: ubuntu-16.04
+ steps:
+ - uses: actions/checkout@v2
+ - name: Run Cypress
+ uses: cypress-io/github-action@v1
+ with:
+ config: baseUrl=${{ needs.wait_deploy.outputs.preview_url }}
+ wait-on: ${{ needs.wait_deploy.outputs.preview_url }}
+ wait-on-timeout: 120
+ # after the test run completes
+ # store videos and any screenshots
+ # NOTE: screenshots will be generated only if E2E test failed
+ # thus we store screenshots only on failures
+ - uses: actions/upload-artifact@v2
+ if: failure()
+ with:
+ name: cypress-screenshots
+ path: cypress/screenshots
+ # Test run video was always captured, so this action uses "always()" condition
+ - uses: actions/upload-artifact@v2
+ if: always()
+ with:
+ name: cypress-videos
+ path: cypress/videos
diff --git a/.gitignore b/.gitignore
index fd07293..a19c5fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,32 +1,162 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+# Created by https://www.toptal.com/developers/gitignore/api/now,yarn,node,macos
+# Edit at https://www.toptal.com/developers/gitignore?templates=now,yarn,node,macos
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
+### macOS ###
+# General
.DS_Store
+.AppleDouble
+.LSOverride
-# debug
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Node ###
+# Logs
+logs
+*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+lerna-debug.log*
-# local env files
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-.now
\ No newline at end of file
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+### now ###
+.now
+
+### yarn ###
+# https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored
+
+# .yarn/unplugged and .yarn/build-state.yml should likely always be ignored since
+# they typically hold machine-specific build artifacts. Ignoring them might however
+# prevent Zero-Installs from working (to prevent this, set enableScripts to false).
+.yarn/unplugged
+.yarn/build-state.yml
+
+# .yarn/cache and .pnp.* may be safely ignored, but you'll need to run yarn install
+# to regenerate them between each branch switch.
+# Uncomment the following lines if you're not using Zero-Installs:
+# .yarn/cache
+# .pnp.*
+
+# End of https://www.toptal.com/developers/gitignore/api/now,yarn,node,macos
+
+# Storybook
+storybook-static
\ No newline at end of file
diff --git a/.huskyrc.json b/.huskyrc.json
index 6091775..932578e 100644
--- a/.huskyrc.json
+++ b/.huskyrc.json
@@ -1,7 +1,7 @@
{
- "hooks": {
- "pre-commit": "lint-staged",
- "pre-push": "npm run lint",
- "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
- }
+ "hooks": {
+ "pre-commit": "lint-staged",
+ "pre-push": "npm run lint",
+ "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
+ }
}
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..9cd25a1
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+12.18.3
diff --git a/.storybook/main.js b/.storybook/main.js
index 44cf475..4d89fb4 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -2,11 +2,8 @@ const path = require('path')
// Export a function. Accept the base config as the only param.
module.exports = {
- webpackFinal: async (baseConfig) => {
- const nextConfig = require('../next.config.js')
-
- // Make whatever fine-grained changes you need
- baseConfig.module.rules.push({
+ webpackFinal: async (config) => {
+ config.module.rules.push({
test: /\.scss$/,
use: [
'style-loader',
@@ -23,6 +20,6 @@ module.exports = {
})
// merge whatever from nextConfig into the webpack config storybook will use
- return { ...baseConfig }
+ return config
},
}
diff --git a/.stylelintrc.json b/.stylelintrc.json
index f52b8ef..5f77f7b 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -5,8 +5,14 @@
"stylelint-config-css-modules",
"stylelint-config-prettier"
],
- "plugins": ["stylelint-prettier"],
+ "plugins": ["stylelint-scss", "stylelint-prettier"],
"rules": {
- "prettier/prettier": true
+ "prettier/prettier": true,
+ "at-rule-no-unknown": [
+ true,
+ {
+ "ignoreAtRules": ["mixin", "include"]
+ }
+ ]
}
}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..66d552a
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+ "recommendations": [
+ "editorconfig.editorconfig",
+ "dbaeumer.vscode-eslint",
+ "syler.sass-indented",
+ "stylelint.vscode-stylelint",
+ "knisterpeter.vscode-commitizen"
+ ],
+ "unwantedRecommendations": ["esbenp.prettier-vscode"]
+}
diff --git a/README.md b/README.md
index d04b8f6..f078289 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
A small online tool to help you generate your favicon in all the sizes and formats you need.
-[](#contributors-)
+[](#contributors-)


@@ -110,14 +110,15 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
(({ template }, ref) => {
const htmlCode = Prism.highlight(template, Prism.languages.markup, 'markup')
return (
-
+
)
-}
+})
+
+CodeHighlight.displayName = 'CodeHighlight'
export { CodeHighlight }
diff --git a/components/dark-mode-toggle/index.module.scss b/components/dark-mode-toggle/index.module.scss
index 549d29e..45db760 100644
--- a/components/dark-mode-toggle/index.module.scss
+++ b/components/dark-mode-toggle/index.module.scss
@@ -72,19 +72,23 @@
.sun,
.moon {
path {
- fill: #657584;
+ fill: $grey-dark;
}
}
}
:global(.light-mode) .label {
- &:hover path {
- fill: $link;
+ @media (pointer: fine) {
+ &:hover path {
+ fill: $link;
+ }
}
}
:global(.dark-mode) .label {
- &:hover path {
- fill: $link-dark;
+ @media (pointer: fine) {
+ &:hover path {
+ fill: $link-dark;
+ }
}
}
diff --git a/components/drag-and-drop/index.module.scss b/components/drag-and-drop/index.module.scss
index 949baf1..fc458f3 100644
--- a/components/drag-and-drop/index.module.scss
+++ b/components/drag-and-drop/index.module.scss
@@ -9,10 +9,28 @@
border-radius: 8px;
box-shadow: 0 2px 4px rgba($black, 0.12);
transition: background 0.2s ease;
+
+ @include breakpoint-md {
+ max-width: 100%;
+ height: 100%;
+ border-radius: 0;
+ }
+
+ &.transparent {
+ @include breakpoint-md {
+ background: transparent;
+ }
+ }
}
:global(.dark-mode) .root {
background: $dark-black;
+
+ &.transparent {
+ @include breakpoint-md {
+ background: transparent;
+ }
+ }
}
.container {
@@ -25,6 +43,21 @@
pointer-events: none;
opacity: 0.32;
}
+
+ @include breakpoint-md {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: calc(100% - 68px);
+ margin-top: 68px;
+ border-radius: 0;
+ }
+
+ &.transparent {
+ @include breakpoint-md {
+ padding: 0;
+ }
+ }
}
.dashed {
@@ -67,6 +100,13 @@
border-radius: 6px;
transition: background 0.2s ease;
+ @include breakpoint-md {
+ flex-grow: 1;
+ margin-bottom: 0;
+ background: transparent;
+ border-radius: 0;
+ }
+
&:hover {
cursor: pointer;
@@ -79,6 +119,10 @@
:global(.dark-mode) .dropZoneWrapper {
background: $dark-grey;
+ @include breakpoint-md {
+ background: transparent;
+ }
+
&:hover {
.dashed rect {
stroke: $off-white-dark;
@@ -95,6 +139,32 @@
}
}
+.dropZoneFooter {
+ transition: background 0.2s ease;
+
+ @include breakpoint-md {
+ padding: 16px 24px 24px;
+ padding-bottom: env(safe-area-inset-bottom);
+ background: rgba($white, 0.88);
+ border-radius: 16px 16px 0 0;
+ box-shadow: 0 -2px 4px rgba($black, 0.12);
+
+ @supports (padding: max(1px)) {
+ padding-bottom: max(24px, env(safe-area-inset-bottom));
+ }
+
+ p {
+ font-size: 15px;
+ }
+ }
+}
+
+:global(.dark-mode) .dropZoneFooter {
+ @include breakpoint-md {
+ background: rgba($dark-black, 0.88);
+ }
+}
+
.dropZone {
position: relative;
z-index: 2;
@@ -126,25 +196,55 @@
&.dragActive {
height: 48px;
}
+
+ @include breakpoint-md {
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 28px;
+ color: $off-white-dark;
+ }
+
+ &.mobileUploadText {
+ @include breakpoint-md-up {
+ display: none;
+ }
+ }
+
+ &.desktopUploadText {
+ /* smartphones, touchscreens */
+ @media (hover: none) and (pointer: coarse) {
+ display: none;
+ }
+ @include breakpoint-md {
+ display: none;
+ }
+ }
}
:global(.dark-mode) .imageUploadText {
color: $white-dark;
+
+ @include breakpoint-md {
+ color: $off-white-dark;
+ }
}
.info {
display: flex;
- align-items: center;
+ align-items: flex-start;
margin-top: 8px;
letter-spacing: 0.2px;
svg {
flex: 0 0 20px;
margin-right: 8px;
+
+ @include breakpoint-md {
+ height: 24px;
+ }
}
strong {
- padding: 0 4px;
font-weight: 700;
}
}
@@ -209,6 +309,7 @@
.imageInfo {
display: flex;
+ flex-wrap: wrap;
padding: 16px 0;
border-top: 1px solid $light-grey;
transition: all 0.2s ease;
@@ -222,17 +323,16 @@
padding: 8px 0 16px;
border-top: 1px solid $light-grey;
transition: all 0.2s ease;
+
+ @include breakpoint-md {
+ padding: 24px 0;
+ }
}
:global(.dark-mode) .imageOptionsWrapper {
border-top: 1px solid $dark-grey;
}
-.imageOptions {
- display: flex;
- padding: 8px 0 16px;
-}
-
.imageInfoItem {
display: flex;
align-items: center;
@@ -241,6 +341,24 @@
span {
margin-right: 8px;
}
+
+ @include breakpoint-md {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+}
+
+.imageOptions {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 8px 0 16px;
+
+ .imageInfoItem {
+ @include breakpoint-md {
+ margin-top: 0;
+ margin-bottom: 24px;
+ }
+ }
}
.pwaCheck {
@@ -259,6 +377,10 @@
align-items: center;
justify-content: center;
margin: 16px 0 12px;
+
+ @include breakpoint-md {
+ align-items: flex-end;
+ }
}
.preview {
@@ -269,15 +391,39 @@
background: $white;
box-shadow: 0 2px 4px rgba($black, 0.12);
+ @include breakpoint-md {
+ width: 96px;
+ height: 96px;
+ margin-right: 0;
+ }
+
img {
width: 40px;
height: 40px;
object-fit: contain;
+
+ @include breakpoint-md {
+ width: 96px;
+ height: 96px;
+ }
}
&.circle,
&.circle img {
border-radius: 24px;
+
+ @include breakpoint-md {
+ width: 52px;
+ height: 52px;
+ border-radius: 26px;
+ }
+ }
+
+ &.circle {
+ @include breakpoint-md {
+ z-index: 1;
+ transform: translate3d(21px, 3px, 0);
+ }
}
&.square,
@@ -285,12 +431,31 @@
width: 48px;
height: 48px;
border-radius: 4px;
+
+ @include breakpoint-md {
+ width: 96px;
+ height: 96px;
+ border-radius: 8px;
+ }
}
&.rounded,
&.rounded img {
margin-right: 0;
border-radius: 14px;
+
+ @include breakpoint-md {
+ width: 40px;
+ height: 40px;
+ border-radius: 13px;
+ }
+ }
+
+ &.rounded {
+ @include breakpoint-md {
+ z-index: 1;
+ transform: translate3d(-12px, 3px, 0);
+ }
}
}
@@ -339,14 +504,32 @@
padding: 23px 24px 24px 24px;
border-top: 1px solid $light-grey;
transition: all 0.2s ease;
+
+ &.spaceBetween {
+ justify-content: space-between;
+ }
+
+ @include breakpoint-md {
+ flex-direction: column-reverse;
+ padding: 11px 24px 12px 24px;
+ border-top: 1px solid transparent;
+
+ > * {
+ margin-bottom: 12px;
+ }
+ }
+
+ @include breakpoint-xs {
+ align-items: stretch;
+ }
}
:global(.dark-mode) .imageFooter {
border-top: 1px solid $dark-grey;
-}
-.spaceBetween {
- justify-content: space-between;
+ @include breakpoint-md {
+ border-top: 1px solid transparent;
+ }
}
.makeNewOne {
@@ -377,12 +560,20 @@
.imageGenerate {
margin-left: 24px;
+
+ @include breakpoint-md {
+ margin-left: 0;
+ }
}
.modalContainer {
max-width: 730px;
height: 100%;
margin: 0 auto;
+
+ @include breakpoint-md {
+ padding-top: 20px;
+ }
}
.modalHeader {
@@ -392,13 +583,23 @@
margin-bottom: 10px;
}
-.modalCode {
- height: 100%;
- max-height: 488px;
- padding: 28px;
- color: $white;
- background: $darker-grey;
- border-radius: 8px;
+.modalHeaderMobile {
+ display: none;
+ padding: 36px 32px 8px;
+ margin-bottom: 0;
+
+ @include breakpoint-md {
+ display: flex;
+ background: $dark-black;
+ border-top-left-radius: 20px;
+ border-top-right-radius: 20px;
+ }
+}
+
+.modalHeaderDesktop {
+ @include breakpoint-md {
+ display: none;
+ }
}
.overlay {
@@ -422,8 +623,8 @@
width: 100%;
max-width: 730px;
height: 100%;
- max-height: 536px;
- padding: 0;
+ max-height: 538px;
+ padding: 2px 0 0;
overflow: auto;
background: transparent;
border: none;
@@ -431,6 +632,11 @@
outline: none;
transform: translate(-50%, -50%);
-webkit-overflow-scrolling: touch;
+
+ @include breakpoint-md {
+ max-height: 100vh;
+ padding: 0;
+ }
}
.copyWrapper {
@@ -460,3 +666,73 @@
:global(.dark-mode) .copyWrapper::before {
color: $off-white-dark;
}
+
+.modalCode {
+ position: relative;
+ height: 100%;
+ max-height: 488px;
+ padding: 28px;
+ color: $white;
+ background: $dark-black;
+ border-radius: 8px;
+
+ @include breakpoint-md {
+ max-height: calc(100% - 84px);
+ border-radius: 0;
+ }
+
+ &::before {
+ z-index: 2;
+ @include breakpoint-md {
+ position: absolute;
+ right: -1px;
+ bottom: 28px;
+ left: 0;
+ height: 136px;
+ content: '';
+ background: linear-gradient(to bottom, rgba($dark-black, 0), rgba($dark-black, 1));
+ }
+ }
+
+ .copyWrapper {
+ z-index: 3;
+ display: none;
+
+ @include breakpoint-md {
+ position: absolute;
+ right: 24px;
+ bottom: 80px;
+ left: 24px;
+ display: block;
+
+ button {
+ width: 100%;
+ }
+ }
+ }
+
+ pre {
+ position: relative;
+ z-index: 1;
+
+ @include breakpoint-md {
+ max-height: calc(100% - 68px);
+ padding-bottom: 104px;
+ }
+ }
+
+ pre code {
+ @include breakpoint-md {
+ white-space: pre-wrap;
+ }
+ }
+}
+
+.modalCodeTitle {
+ display: none;
+
+ @include breakpoint-md {
+ display: block;
+ margin-bottom: 20px;
+ }
+}
diff --git a/components/drag-and-drop/index.tsx b/components/drag-and-drop/index.tsx
index da06b10..646bc52 100644
--- a/components/drag-and-drop/index.tsx
+++ b/components/drag-and-drop/index.tsx
@@ -1,9 +1,11 @@
import dynamic from 'next/dynamic'
-import React, { useState, useEffect, useCallback } from 'react'
+import React, { useState, useEffect, useCallback, useRef } from 'react'
import classnames from 'classnames'
import { useDropzone, FileRejection } from 'react-dropzone'
import { CSSTransition } from 'react-transition-group'
+import { useDrag } from 'react-use-gesture'
import { headTemplate } from 'utils/favicon'
+import { isTouchCapable } from 'utils/device'
import { Typography } from 'components/typography'
import { CodeHighlight } from 'components/code-highlight'
import { SvgDropZone } from 'components/svgs/svg-drop-zone'
@@ -23,6 +25,8 @@ 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 { useToggle } from 'hooks/use-toggle'
+import { useMediaQueryContext } from 'components/media-query-provider'
import styles from './index.module.scss'
@@ -31,11 +35,12 @@ const Clipboard = dynamic(() => import('react-clipboard.js'))
type DragAndDropProps = {
onFile: (hasFile: boolean) => void
- onGenerate: (image: File, pwa: boolean) => Promise
+ onGenerate: (image: File, pwa: boolean, darkMode: boolean) => Promise
onError: (message: string) => void
}
const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
+ const { isMobile } = useMediaQueryContext()
const [imageSizes, setImageSizes] = useState({ width: 0, height: 0 })
const [image, setImage] = useState()
const [imageBase64, setImageBase64] = useState('')
@@ -48,10 +53,11 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
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 [pwa, togglePwa] = useToggle(false)
+ const [darkMode, toggleDarkMode] = useToggle(false)
const [zipData, setZipData] = useState()
const [isModalOpen, setIsModalOpen] = useState(false)
+ const modalBodyRef = useRef(null)
const [copied, setCopied] = useState(false)
const PWADisabled = !isSvg && !is512px
let sizesCount = 16
@@ -62,6 +68,22 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
sizesCount += 3
}
+ const bind = useDrag(
+ (state) => {
+ const {
+ swipe, // [swipeX, swipeY] 0 if no swipe detected, -1 or 1 otherwise
+ } = state
+ const isSwipeDown = swipe[1] === 1
+ if (isSwipeDown && modalBodyRef.current?.scrollTop === 0) {
+ setIsModalOpen(false)
+ }
+ },
+ {
+ enabled: isTouchCapable(),
+ filterTaps: true,
+ }
+ )
+
const generateImgFromCanvas = useCallback(() => {
const canvas = document.createElement('canvas')
canvas.width = imageSizes.width
@@ -84,32 +106,37 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
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)
+ const length = isMobile ? 15 : 30
+ const name = matches[1].length > length ? `${matches[1].slice(0, length)}..` : matches[1].slice(0, length)
setImageData({ name, extension: matches[2] })
}
- }, [image, zipData, generateImgFromCanvas])
+ }, [image, zipData, generateImgFromCanvas, isMobile])
useEffect(() => {
if (!image) {
return
- } else if (isPngOrSvg && isSquare) {
+ } else if ((isPngOrSvg && isSquare && is310px) || isSvg) {
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 if (isSquare && isPngOrSvg && !is310px && !isSvg) {
+ setTitle('Your image should be higher than 310px')
+ } else if ((isSquare && !isPngOrSvg && !is310px) || (!isSquare && !isPngOrSvg && is310px)) {
+ setTitle('Your image has a few problems')
+ } else if (isSquare && !isPngOrSvg && is310px) {
+ setTitle('Your image should be in PNG or SVG')
+ } else if (!isSquare && isPngOrSvg && is310px) {
+ setTitle('Your image should be squared')
} else {
setTitle('Your image is not in a good place')
}
- }, [image, isSquare, is310px, isPngOrSvg])
+ }, [image, isSquare, is310px, isPngOrSvg, isSvg])
const generateFavicon = async (file: File) => {
try {
setIsLoading(true)
- const zip = await onGenerate(file, pwa)
+ const zip = await onGenerate(file, pwa, darkMode)
setZipData(zip)
} catch (error) {
- console.log(error)
+ onError(error.message || 'Something went wrong. Please try again.')
} finally {
setIsLoading(false)
}
@@ -167,8 +194,12 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
const [isHover, setIsHover] = useState(false)
return (
-
-
+
+
{!image && !zipData && (
<>
{
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}>
-
+
@@ -184,7 +215,23 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
+ className={classnames(styles.imageUploadText, styles.mobileUploadText, {
+ [styles.dragActive]: isDragActive,
+ })}
+ data-cy="drag-and-drop-text">
+
+ Generate all the sizes for your
+
+ favicon. Tap to upload a file.
+
+
+
Drag &
@@ -206,13 +253,17 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
- {/*
*/}
-
- We recommend a square PNG or SVG with at least 310px
-
-
- No data or images are stored
-
+
+
+ {' '}
+
+ We recommend a square PNG or SVG with at least 310px
+
+
+
+ No data or images are stored
+
+
>
)}
{image && !zipData && (
@@ -223,7 +274,7 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
-
+
{title}
{
color="gray"
tag="div"
className={styles.filenameWrapper}>
-
+
{imageData.name}
{imageData.extension}
-
+
({imageSizes.width}x{imageSizes.height})
@@ -244,19 +295,21 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
- {isSquare ? : }
+ {isSquare ? : }
Square image
- {isPngOrSvg ? : }
+ {isPngOrSvg ? : }
PNG or SVG
-
{isSvg || is310px ? : }
+
+ {isSvg || is310px ? : }
+
310px or higher
@@ -268,7 +321,12 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
- setPwa(!pwa)}>
+
PWA compatible
@@ -279,12 +337,17 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
- setDarkMode(!darkMode)}>
+
Dark Mode version
- Available soon
+ Coming soon
@@ -358,37 +421,52 @@ const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => {
Download Favicon
-
setIsModalOpen(false)}
- className={styles.content}
- overlayClassName={styles.overlay}
- closeTimeoutMS={200}
- contentLabel="Code generated">
-
-
-
- Insert the following code in the <head> section of your pages:
-
-
-
-
-
-
-
-
-
-
-
-
>
)}
+
setIsModalOpen(false)}
+ className={styles.content}
+ overlayClassName={styles.overlay}
+ closeTimeoutMS={200}
+ contentLabel="Code generated">
+
+
+
+ Code
+
+
+
+
+
+ Insert the following code in the <head> section of your pages:
+
+
+
+
+
+
+
+
+
+ Insert the following code in the <head> section of your pages:
+
+
+
+
+
+
+
+
+
+
)
diff --git a/components/favycon-error/index.module.scss b/components/favycon-error/index.module.scss
index 1bb4e3c..41f4cb1 100644
--- a/components/favycon-error/index.module.scss
+++ b/components/favycon-error/index.module.scss
@@ -4,6 +4,7 @@
position: absolute;
top: 26px;
right: 72px;
+ z-index: 10;
display: flex;
align-items: center;
padding: 6px 13px 6px 16px;
@@ -11,6 +12,21 @@
border-radius: 8px;
box-shadow: 0 2px 4px rgba($black, 0.12);
+ @include breakpoint-md {
+ top: 13px;
+ right: 12px;
+ left: 12px;
+ justify-content: center;
+ max-width: 400px;
+ margin: auto;
+ }
+
+ .content {
+ @include breakpoint-md {
+ font-size: 14px;
+ }
+ }
+
svg {
margin-left: 13px;
}
diff --git a/components/favycon-error/index.tsx b/components/favycon-error/index.tsx
index 8bc6d26..4094c5b 100644
--- a/components/favycon-error/index.tsx
+++ b/components/favycon-error/index.tsx
@@ -17,12 +17,12 @@ const FavyconError = ({ error, clearError }: FavyconErrorProps) => {
if (error) {
let endTimeout: number
setShowError(true)
- const errorTimeout = setTimeout(() => {
+ const errorTimeout = window.setTimeout(() => {
setShowError(false)
- endTimeout = setTimeout(() => {
+ endTimeout = window.setTimeout(() => {
clearError()
- }, 300) as any
- }, 5300) as any
+ }, 300)
+ }, 5300)
return () => {
clearTimeout(errorTimeout)
clearTimeout(endTimeout)
@@ -33,7 +33,12 @@ const FavyconError = ({ error, clearError }: FavyconErrorProps) => {
return (
-
+
{error}