diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..8d72ef2a --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,3 @@ +.yalc/ +.yalcignore +yalc.lock \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5b685159..ecc99300 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.1.2", "license": "UNLICENSED", "dependencies": { + "@jboolean/react-zoom-pan-pinch": "^3.7.2", "@sentry/react": "^7.119.2", "@sentry/tracing": "^7.114.0", "@stripe/stripe-js": "^1.42.0", @@ -114,6 +115,19 @@ "node": "^20" } }, + ".yalc/react-zoom-pan-pinch": { + "version": "0.0.0", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -2108,6 +2122,19 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@jboolean/react-zoom-pan-pinch": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@jboolean/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.2.tgz", + "integrity": "sha512-eMMTjp5aRnuAe8n+Vu7BrSomHbY2RM+2L3XvMFlKIl/mKpMCuSJ/EXTFTUSkZj+U8Kiy1637ZCeAPUaXxAbHlQ==", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -14781,6 +14808,12 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@jboolean/react-zoom-pan-pinch": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@jboolean/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.2.tgz", + "integrity": "sha512-eMMTjp5aRnuAe8n+Vu7BrSomHbY2RM+2L3XvMFlKIl/mKpMCuSJ/EXTFTUSkZj+U8Kiy1637ZCeAPUaXxAbHlQ==", + "requires": {} + }, "@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index ba57db56..86ed6824 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "author": "Julian Boilen", "license": "UNLICENSED", "dependencies": { + "@jboolean/react-zoom-pan-pinch": "^3.7.2", "@sentry/react": "^7.119.2", "@sentry/tracing": "^7.114.0", "@stripe/stripe-js": "^1.42.0", diff --git a/frontend/src/screens/App/screens/AnnouncementBanner/AnnouncementRegistry.tsx b/frontend/src/screens/App/screens/AnnouncementBanner/AnnouncementRegistry.tsx index 61c85c39..a5087a34 100644 --- a/frontend/src/screens/App/screens/AnnouncementBanner/AnnouncementRegistry.tsx +++ b/frontend/src/screens/App/screens/AnnouncementBanner/AnnouncementRegistry.tsx @@ -2,6 +2,16 @@ import React from 'react'; import Announcment from './Announcement'; const ANNOUNCEMENTS_REGISTRY: Announcment[] = [ + { + id: 'zoom', + expiresAt: new Date('2025-01-31'), + render: () => ( + + 🔍 What’s that sign say? Is that mom? New: zoom in and find + out. + + ), + }, { id: 'high-quality-imagery', expiresAt: new Date('2024-09-05'), diff --git a/frontend/src/screens/App/screens/AnnouncementBanner/index.tsx b/frontend/src/screens/App/screens/AnnouncementBanner/index.tsx index 28c268a2..6a340cec 100644 --- a/frontend/src/screens/App/screens/AnnouncementBanner/index.tsx +++ b/frontend/src/screens/App/screens/AnnouncementBanner/index.tsx @@ -19,7 +19,7 @@ export default function AnnouncementBanner(): JSX.Element | null {
{announcementToDisplay.render()}{' '}
); diff --git a/frontend/src/screens/App/screens/ViewerPane/ViewerPane.less b/frontend/src/screens/App/screens/ViewerPane/ViewerPane.less index ea7a89ac..c7e5ebbd 100644 --- a/frontend/src/screens/App/screens/ViewerPane/ViewerPane.less +++ b/frontend/src/screens/App/screens/ViewerPane/ViewerPane.less @@ -15,29 +15,14 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } - .colorLayer { - z-index: 1; - position: absolute; - - user-select: none; - pointer-events: none; - - // We get low res color images and "upscole" them here by only using the color information - // and blending in the lightness of the full image - mix-blend-mode: color; - } - - .image, - .colorLayer { - object-fit: contain; - object-position: center center; + .imageStack { width: 100%; height: 100%; - transform: scale(1.05); // cuts off the borders } .overlay { z-index: 2; + touch-action: none; } @content-padding: 5.5px; @@ -48,7 +33,7 @@ grid-template-columns: 100%; grid-template-areas: 'alternates' - '.' + 'zoomHitArea' 'stories' 'buttons' 'credit'; @@ -84,6 +69,9 @@ border: none; cursor: pointer; + + // drop shadow + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); } .alternates { @@ -111,6 +99,8 @@ box-sizing: border-box; color: white; + + user-select: all; } .buttons { @@ -146,4 +136,35 @@ // https://bugs.chromium.org/p/chromium/issues/detail?id=1194050 // Maybe revisit this later } + + .zoomHitArea { + grid-area: zoomHitArea; + } + + // Class name to exclude from pinch zoom + .panPinchExcluded { + user-select: text; + } + + .panning { + cursor: grabbing !important; + } + + .transformWrapper, + .transformContent { + width: 100% !important; + height: 100% !important; + } + + .zoomable { + cursor: zoom-in; + + & > * { + cursor: auto; + } + + &.zoomed { + cursor: zoom-out; + } + } } diff --git a/frontend/src/screens/App/screens/ViewerPane/components/ColorLayer.less b/frontend/src/screens/App/screens/ViewerPane/components/ColorLayer.less index 42c7a71e..7a6678f3 100644 --- a/frontend/src/screens/App/screens/ViewerPane/components/ColorLayer.less +++ b/frontend/src/screens/App/screens/ViewerPane/components/ColorLayer.less @@ -12,6 +12,16 @@ .colorLayer { // default state .faded(); + + z-index: 1; + position: absolute; + + user-select: none; + pointer-events: none; + + // We get low res color images and "upscale" them here by only using the color information + // and blending in the lightness of the full image + mix-blend-mode: color; } .enter, diff --git a/frontend/src/screens/App/screens/ViewerPane/components/ColorLayer.tsx b/frontend/src/screens/App/screens/ViewerPane/components/ColorLayer.tsx index b0a14e00..0d67be26 100644 --- a/frontend/src/screens/App/screens/ViewerPane/components/ColorLayer.tsx +++ b/frontend/src/screens/App/screens/ViewerPane/components/ColorLayer.tsx @@ -12,7 +12,7 @@ export default function ColorLayer({ className, }: { photoIdentifier: string; - className: string; + className?: string; }): JSX.Element | null { const { colorEnabledForIdentifier, @@ -49,7 +49,6 @@ export default function ColorLayer({ className={classNames(stylesheet.colorLayer, className)} src={colorizedImageSrc} onLoad={() => { - console.log('loaded', enabled); if (enabled) { handleImageLoaded(); } diff --git a/frontend/src/screens/App/screens/ViewerPane/components/ImageStack.less b/frontend/src/screens/App/screens/ViewerPane/components/ImageStack.less new file mode 100644 index 00000000..16627212 --- /dev/null +++ b/frontend/src/screens/App/screens/ViewerPane/components/ImageStack.less @@ -0,0 +1,14 @@ +:local { + .image { + display: block; + object-fit: contain; + object-position: center center; + width: 100%; + height: 100%; + } + + .overlayLayer { + z-index: 1; + position: absolute; + } +} diff --git a/frontend/src/screens/App/screens/ViewerPane/components/ImageStack.tsx b/frontend/src/screens/App/screens/ViewerPane/components/ImageStack.tsx new file mode 100644 index 00000000..a4c4af19 --- /dev/null +++ b/frontend/src/screens/App/screens/ViewerPane/components/ImageStack.tsx @@ -0,0 +1,83 @@ +import React, { ImgHTMLAttributes } from 'react'; + +import classNames from 'classnames'; +import { PHOTO_BASE } from 'shared/utils/apiConstants'; +import preloadImage from 'shared/utils/preloadImage'; +import ColorLayer from './ColorLayer'; +import stylesheet from './ImageStack.less'; +import { View } from './ImageSwitcher'; + +type Props = { + photoIdentifier: string; + imgProps?: Omit, 'src'>; + className: string; + isFullResVisible?: boolean; +}; + +type ImageFormat = '720-jpg' | '420-jpg' | 'jpg'; + +const forgeImgSrc = ( + photoIdentifier: string, + format: ImageFormat = '720-jpg' +): string => `${PHOTO_BASE}/${format}/${photoIdentifier}.jpg`; + +function HighResLayer({ + photoIdentifier, +}: { + photoIdentifier: string; +}): JSX.Element { + const [loaded, setLoaded] = React.useState(false); + + return ( + setLoaded(true)} + style={{ opacity: loaded ? 1 : 0 }} + /> + ); +} + +/** + * An ImageStack keeps all the layers for one photo aligned and combined so they can be treated as one. + * Layers: Base layer, high res layer, color layer. + */ +export default function ImageStack({ + photoIdentifier, + imgProps, + className, + isFullResVisible, +}: Props): JSX.Element | null { + const baseImageSrc = forgeImgSrc(photoIdentifier); + + return ( +
+ {isFullResVisible && ( + + )} + + +
+ ); +} +export function makeImageSwitcherView(props: Props): View { + const imgSrc = forgeImgSrc(props.photoIdentifier); + return { + key: props.photoIdentifier, + element: , + preload: () => preloadImage(imgSrc), + }; +} diff --git a/frontend/src/screens/App/screens/ViewerPane/components/ImageSwitcher.tsx b/frontend/src/screens/App/screens/ViewerPane/components/ImageSwitcher.tsx index 24fa6657..6cb7fe99 100644 --- a/frontend/src/screens/App/screens/ViewerPane/components/ImageSwitcher.tsx +++ b/frontend/src/screens/App/screens/ViewerPane/components/ImageSwitcher.tsx @@ -5,14 +5,21 @@ import recordEvent from 'shared/utils/recordEvent'; import Require from 'utils/Require'; +export type View = { + key: string; + element: React.ReactElement; + preload: () => Promise; +}; interface State { - lastSrc: string; - visibleSrc: string; + lastView: View; + visibleView: View; hide: boolean; loaded: boolean; } -type Props = Require, 'src'>; +type Props = { + view: View; +}; import stylesheet from './ImageSwitcher.less'; @@ -23,12 +30,27 @@ function preloadImage(url: string, callback: () => void): void { img.addEventListener('error', callback); } +// Make a view object for a basic img element +export function makeImgView( + props: Require, 'src'> +): View { + return { + key: props.src, + element: , + preload: () => new Promise((resolve) => preloadImage(props.src, resolve)), + }; +} + +/** + * ImageSwitcher can transition between any Views that conform to the View interface. + * The goal is to increase perceived load speed by starting an animation immediately while preloading the view. + */ export default class ImageSwitcher extends React.Component { constructor(props: Props) { super(props); this.state = { - lastSrc: props.src, - visibleSrc: props.src, + lastView: props.view, + visibleView: props.view, hide: false, loaded: true, }; @@ -36,17 +58,24 @@ export default class ImageSwitcher extends React.Component { this.handleExited = this.handleExited.bind(this); } - componentDidUpdate(prevProps: Props, prevState: State): void { - if (this.props.src !== prevState.lastSrc && !this.state.hide) { + componentDidUpdate(_prevProps: Props, prevState: State): void { + if (this.props.view.key !== prevState.lastView.key && !this.state.hide) { // eslint-disable-next-line react/no-did-update-set-state this.setState( { hide: true, loaded: false, - lastSrc: this.props.src, + lastView: this.props.view, }, () => { - preloadImage(this.props.src, this.handleNewImgLoad); + this.props.view.preload().then( + () => { + this.handleNewImgLoad(); + }, + (e) => { + console.error('Failed to preload image', e); + } + ); } ); @@ -77,12 +106,18 @@ export default class ImageSwitcher extends React.Component { swapImage(): void { this.setState({ - visibleSrc: this.props.src, + visibleView: this.props.view, }); } render(): JSX.Element { - const { visibleSrc, hide, loaded } = this.state; + const { visibleView, hide, loaded } = this.state; + + // Use the one from props if it is the same key so we get the most up-to-date element + const element = + visibleView.key === this.props.view.key + ? this.props.view.element + : visibleView.element; return ( { timeout={150} onExited={this.handleExited} > - + {element} ); } diff --git a/frontend/src/screens/App/screens/ViewerPane/components/Overlay/Overlay.less b/frontend/src/screens/App/screens/ViewerPane/components/Overlay/Overlay.less index cc0d97e2..345480e0 100644 --- a/frontend/src/screens/App/screens/ViewerPane/components/Overlay/Overlay.less +++ b/frontend/src/screens/App/screens/ViewerPane/components/Overlay/Overlay.less @@ -11,5 +11,7 @@ height: 100%; z-index: 2; + user-select: none; + .fadeTransition(); } diff --git a/frontend/src/screens/App/screens/ViewerPane/components/Overlay/index.tsx b/frontend/src/screens/App/screens/ViewerPane/components/Overlay/index.tsx index a545bb7c..a5833d46 100644 --- a/frontend/src/screens/App/screens/ViewerPane/components/Overlay/index.tsx +++ b/frontend/src/screens/App/screens/ViewerPane/components/Overlay/index.tsx @@ -1,20 +1,33 @@ -import React from 'react'; +import React, { RefObject } from 'react'; -import { CSSTransition } from 'react-transition-group'; import classnames from 'classnames'; +import { CSSTransition } from 'react-transition-group'; -import stylesheet from './Overlay.less'; import { useFeatureFlag } from 'screens/App/shared/stores/FeatureFlagsStore'; import FeatureFlag from 'screens/App/shared/types/FeatureFlag'; +import useCanHover from '../../shared/utils/useCanHover'; +import stylesheet from './Overlay.less'; // Encapsulates overlay logic for fading children in and out export default function Overlay({ className, + overlayRef, children, -}: React.PropsWithChildren<{ className?: string }>): JSX.Element { +}: React.PropsWithChildren<{ + className?: string; + overlayRef?: RefObject; +}>): JSX.Element { // This feature flag is useful in development to prevent the overlay from disappearing const alwaysShowOverlay = useFeatureFlag(FeatureFlag.ALWAYS_SHOW_OVERLAY); + // Okay so, my Android phone is reporting hover events of type "mouse" + // This will let us ignore those events on devices that cannot hover + const canHover = useCanHover(); + + const startXRef = React.useRef(0); + const startYRef = React.useRef(0); + const pointerIdRef = React.useRef(null); + const [isOverlayVisible, setIsOverlayVisible] = React.useState(alwaysShowOverlay); const timeoutRef = React.useRef(null); @@ -39,10 +52,10 @@ export default function Overlay({ } }; - const handleStartHover: React.PointerEventHandler = ( - e - ): void => { - if (e.pointerType !== 'mouse') { + const handleStartHover: React.PointerEventHandler< + HTMLDivElement + > = (): void => { + if (!canHover) { return; } setIsOverlayVisible(true); @@ -50,17 +63,41 @@ export default function Overlay({ }; const handlePointerDown: React.PointerEventHandler = (e) => { - if (e.target !== e.currentTarget) { + pointerIdRef.current = e.pointerId; + startXRef.current = e.clientX; + startYRef.current = e.clientY; + }; + + const handlePointerUp: React.PointerEventHandler = (e) => { + if (e.pointerId !== pointerIdRef.current) { + return; + } + if (canHover) { + return; + } + + // only count events on overlay or overlaycontent + if ( + e.target !== overlayRef.current && + e.target !== overlayRef.current?.firstChild + ) { + return; + } + const deltaX = e.clientX - startXRef.current; + const deltaY = e.clientY - startYRef.current; + const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2); + // If the pointer moved more than 10 pixels, ignore + if (distance > 10) { return; } setIsOverlayVisible(!isOverlayVisible); cancelPeekTimeout(); }; - const handleEndHover: React.PointerEventHandler = ( - e - ): void => { - if (e.pointerType !== 'mouse') { + const handleEndHover: React.PointerEventHandler< + HTMLDivElement + > = (): void => { + if (!canHover) { return; } if (alwaysShowOverlay) { @@ -75,6 +112,8 @@ export default function Overlay({ onPointerOver={handleStartHover} onPointerLeave={handleEndHover} onPointerDown={handlePointerDown} + onPointerUp={handlePointerUp} + ref={overlayRef} > -
- {children} -
+
{children}
); diff --git a/frontend/src/screens/App/screens/ViewerPane/index.tsx b/frontend/src/screens/App/screens/ViewerPane/index.tsx index 295f3b90..72f810ed 100644 --- a/frontend/src/screens/App/screens/ViewerPane/index.tsx +++ b/frontend/src/screens/App/screens/ViewerPane/index.tsx @@ -1,19 +1,81 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import classnames from 'classnames'; +import { + InnerTransformedContent, + ReactZoomPanPinchContentRef, + ReactZoomPanPinchProps, + TransformComponent, + TransformWrapper, +} from '@jboolean/react-zoom-pan-pinch'; +import { useHistory, useParams } from 'react-router'; +import Alternates from './components/Alternates'; +import ImageButtons from './components/ImageButtons'; +import * as ImageStack from './components/ImageStack'; import ImageSwitcher from './components/ImageSwitcher'; +import Overlay from './components/Overlay'; +import Stories from './components/Stories'; import stylesheet from './ViewerPane.less'; +import useCanHover from './shared/utils/useCanHover'; -import { useHistory, useParams } from 'react-router'; -import Alternates from './components/Alternates'; -import Overlay from './components/Overlay'; +const INITIAL_SCALE = 1.05; -import { PHOTO_BASE } from 'shared/utils/apiConstants'; -import ImageButtons from './components/ImageButtons'; -import Stories from './components/Stories'; -import ColorLayer from './components/ColorLayer'; +const ClickToZoomHitArea = ({ + wrapper, + isZoomed, + isPanning, +}: { + wrapper: ReactZoomPanPinchContentRef; + isZoomed: boolean; + isPanning: boolean; +}): JSX.Element | null => { + const canHover = useCanHover(); + + const startXRef = React.useRef(0); + const startYRef = React.useRef(0); + + const handleZoomHitAreaMouseDown = ( + e: React.MouseEvent + ): void => { + startXRef.current = e.clientX; + startYRef.current = e.clientY; + }; + + const handleZoomHitAreaClick = ( + e: React.MouseEvent + ): void => { + if (!wrapper) return; + const { clientX, clientY } = e; + + const deltaX = clientX - startXRef.current; + const deltaY = clientY - startYRef.current; + const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2); + // If the pointer moved more than 10 pixels, ignore + if (distance > 10) { + return; + } + + if (isZoomed) wrapper.resetTransform(); + else wrapper.zoomToPoint(clientX, clientY, 3.8, 200, 'easeOut'); + }; + + if (!canHover) { + return null; + } + + return ( +
+ ); +}; export default function ViewerPane({ className, @@ -23,47 +85,123 @@ export default function ViewerPane({ const { identifier: photoIdentifier } = useParams<{ identifier?: string }>(); const history = useHistory(); + const overlayRef = React.useRef(null); + const wrapperRef = React.useRef(null); + const [isZoomed, setIsZoomed] = React.useState(false); + const [isPanning, setIsPanning] = React.useState(false); + const canHover = useCanHover(); + + // Reset the transform when the photo changes + useEffect(() => { + if (wrapperRef.current) wrapperRef.current.resetTransform(); + }, [photoIdentifier, wrapperRef]); + + const handleZoom: ReactZoomPanPinchProps['onTransformed'] = ( + _ref, + state + ): void => { + const scale = state.scale; + + setIsZoomed(scale > INITIAL_SCALE); + }; + return ( -
- -
-
- - - -
- -
- -
- -
- -
- -
- -

- Photo courtesy NYC Municipal Archives{' '} -

-
- - - -
+ setIsPanning(true)} + onPanningStop={() => setIsPanning(false)} + > +
+ + +
+
+ + + +
+ +
+ + + +
+ +
+ +
+ +
+ +

+ Photo courtesy NYC Municipal Archives{' '} +

+
+ + + + + + +
+ ); } diff --git a/frontend/src/screens/App/screens/ViewerPane/shared/utils/useCanHover.ts b/frontend/src/screens/App/screens/ViewerPane/shared/utils/useCanHover.ts new file mode 100644 index 00000000..16dd65ba --- /dev/null +++ b/frontend/src/screens/App/screens/ViewerPane/shared/utils/useCanHover.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; + +export default function useCanHover(): boolean { + const [hasFinePointer, setHasFinePointer] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia('(hover: hover)'); + + // Set initial value + setHasFinePointer(mediaQuery.matches); + + // Define a handler for changes + const handleChange = (event: MediaQueryListEvent): void => { + setHasFinePointer(event.matches); + }; + + // Add event listener for changes + mediaQuery.addEventListener('change', handleChange); + + // Cleanup event listener on unmount + return () => { + mediaQuery.removeEventListener('change', handleChange); + }; + }, []); + + return hasFinePointer; +} diff --git a/frontend/src/shared/components/Carousel/index.tsx b/frontend/src/shared/components/Carousel/index.tsx index 67e64226..ec9bacd8 100644 --- a/frontend/src/shared/components/Carousel/index.tsx +++ b/frontend/src/shared/components/Carousel/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { CSSTransition, SwitchTransition } from 'react-transition-group'; +import preloadImage from 'shared/utils/preloadImage'; import stylesheet from './Carousel.less'; interface Image { @@ -28,6 +29,12 @@ export default function Carousel({ }; }, [images.length]); + // preload next image + React.useEffect(() => { + const nextI = (i + 1) % images.length; + void preloadImage(images[nextI].src); + }, [i, images]); + const image = images[i]; return ( diff --git a/frontend/src/shared/utils/preloadImage.ts b/frontend/src/shared/utils/preloadImage.ts new file mode 100644 index 00000000..a07ffb65 --- /dev/null +++ b/frontend/src/shared/utils/preloadImage.ts @@ -0,0 +1,9 @@ +export default function preloadImage(url: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = url; + + img.addEventListener('load', () => resolve()); + img.addEventListener('error', reject); + }); +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 356155d5..c6dfce67 100755 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "outDir": "dist/src", "noImplicitAny": true, - "noUnusedLocals": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "importHelpers": true, "module": "commonjs", "target": "es5", diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 6c4d6a43..18155227 100755 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -27,20 +27,20 @@ module.exports = { }), new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/ }), new CopyPlugin({ - patterns: [{ from: '_redirects' }, {from: 'terms.html'}], + patterns: [{ from: '_redirects' }, { from: 'terms.html' }], }), - new ESLintPlugin({ fix: true, }), + new ESLintPlugin({ fix: true, exclude: ['node_modules', '.yalc'] }), ], module: { rules: [ { test: /\.jsx?$/, - exclude: /node_modules/, + exclude: /node_modules|.yalc/, use: ['babel-loader'], }, { test: /\.tsx?$/, - exclude: /node_modules/, + exclude: /node_modules|.yalc/, loader: 'ts-loader', }, { @@ -67,7 +67,7 @@ module.exports = { { test: /\.svg$/, issuer: /\.(j|t)sx$/, - exclude: /node_modules/, + exclude: /node_modules|.yalc/, loader: 'svg-react-loader', resourceQuery: { not: [/asset/] }, options: {