diff --git a/.babelrc b/.babelrc index 9189172..3457add 100644 --- a/.babelrc +++ b/.babelrc @@ -1,11 +1,12 @@ { - "presets": [ - "next/babel" - ], + "presets": ["next/babel"], "plugins": [ - ["styled-components", { - "displayName": false, - "ssr": true - }] + [ + "styled-components", + { + "displayName": false, + "ssr": true + } + ] ] } diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..9841c7a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +yarn lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d93ee08 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +.next +node_modules +coverage +dist +build +out +test-results +public/static +.yarn +.pnp.* +yarn.lock diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..578076d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "arrowParens": "avoid", + "printWidth": 120 +} diff --git a/README.md b/README.md index 0adbe08..c6899be 100755 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ In addition to the software requirements, you need access to the following APIs: - [Discogs](https://www.discogs.com/applications/edit) for searching and getting release info - [Last.fm](https://www.last.fm/api/account/create) for authentication and scrobbling data - ### Installing First you need to clone the repository from github: @@ -85,6 +84,7 @@ yarn test ``` You can also use watch-mode and display the current test coverage: + ``` yarn test:watch yarm test:coverage @@ -110,7 +110,7 @@ yarn start ## Authors -* **Daniel Puscher** - *Initial work* - [dpuscher](https://github.com/dpuscher) +- **Daniel Puscher** - _Initial work_ - [dpuscher](https://github.com/dpuscher) See also the list of [contributors](https://github.com/dpuscher/code-scrobble/contributors) who participated in this project. diff --git a/app/__mocks__/redis.ts b/app/__mocks__/redis.ts index 5086d14..13249d7 100644 --- a/app/__mocks__/redis.ts +++ b/app/__mocks__/redis.ts @@ -2,10 +2,10 @@ const store = new Map(); -const mockGet = jest.fn(async (key) => store.get(key) || null); +const mockGet = jest.fn(async key => store.get(key) || null); const mockSet = jest.fn(async (key, value) => { store.set(key, value); - return 'OK'; + return "OK"; }); const mockConnect = jest.fn(async () => {}); const mockOn = jest.fn(); diff --git a/app/cache.ts b/app/cache.ts index 8a99241..9c1deb5 100644 --- a/app/cache.ts +++ b/app/cache.ts @@ -1,11 +1,10 @@ -import { createClient } from 'redis'; +import { createClient } from "redis"; const client = createClient({ url: process.env.REDISCLOUD_URL }); client.connect().catch(console.error); -client.on('error', console.error); +client.on("error", console.error); -export const set = (key: string, value: unknown, ttl = 86400) => - client.set(key, JSON.stringify(value), { EX: ttl }); +export const set = (key: string, value: unknown, ttl = 86400) => client.set(key, JSON.stringify(value), { EX: ttl }); export const get = async (key: string): Promise => { const value = await client.get(key); diff --git a/app/discogs.ts b/app/discogs.ts index c438cdc..43bdf72 100644 --- a/app/discogs.ts +++ b/app/discogs.ts @@ -1,10 +1,10 @@ -import orderBy from 'lodash/orderBy'; -import pick from 'lodash/pick'; -import find from 'lodash/find'; -import * as Cache from './cache'; +import orderBy from "lodash/orderBy"; +import pick from "lodash/pick"; +import find from "lodash/find"; +import * as Cache from "./cache"; // eslint-disable-next-line @typescript-eslint/no-require-imports -const DiscogsClient = require('disconnect').Client; +const DiscogsClient = require("disconnect").Client; interface DiscogsError { statusCode: number; @@ -71,19 +71,17 @@ const Database = new DiscogsClient({ const convertTimecode = (timecode: string | undefined): number => { if (!timecode) return 0; return timecode - .split(':') - .map(n => (parseInt(n, 10) || 0)) + .split(":") + .map(n => parseInt(n, 10) || 0) .reverse() - .map((n, i) => n * (60 ** i)) + .map((n, i) => n * 60 ** i) .reduce((pv, cv) => pv + cv); }; const normalizeTracklist = (tracks: DiscogsTrack[]): DiscogsTrack[] => { const vinylPositionRegex = /^[A-Z]-?[0-9]+$/; // eslint-disable-next-line no-underscore-dangle - let tracklist = tracks - .filter(track => track.type_ === 'track') - .filter(track => !/video/i.test(track.position)); + let tracklist = tracks.filter(track => track.type_ === "track").filter(track => !/video/i.test(track.position)); // Remove Bonus CDs from vinyl releases: if (tracklist.length && vinylPositionRegex.test(tracklist[0].position)) { @@ -97,67 +95,70 @@ const normalizeTracklist = (tracks: DiscogsTrack[]): DiscogsTrack[] => { }; const getBarcode = (data: DiscogsIdentifier[] = []): string | undefined => - (find(data, { type: 'Barcode' }) || {}).value; + (find(data, { type: "Barcode" }) || {}).value; const buildRelease = (id: number, data: DiscogsData): ReleaseData => ({ id, - artist: (data.artists || []).map(a => a.name).join(', '), + artist: (data.artists || []).map(a => a.name).join(", "), title: data.title, image: data?.images?.[0]?.uri, url: data.uri, year: data.year, - tracks: normalizeTracklist(data.tracklist || []) - .map((track, index) => ({ - title: track.title, - trackNumber: index + 1, - duration: convertTimecode(track.duration), - })), + tracks: normalizeTracklist(data.tracklist || []).map((track, index) => ({ + title: track.title, + trackNumber: index + 1, + duration: convertTimecode(track.duration), + })), barcode: getBarcode(data.identifiers), }); export const barcode = (barcodeValue: string): Promise => - new Promise((resolve) => { + new Promise(resolve => { const cacheKey = `barcode--${barcodeValue}`; Cache.get(cacheKey) - .then((result) => { + .then(result => { resolve(result); }) .catch(() => { - Database.search(undefined, { barcode: barcodeValue, type: 'release' }, (err: DiscogsError, data: { results: DiscogsSearchResult[] }) => { - if (err || !data || !data.results || !data.results.length) { - return resolve(undefined); - } - const results = orderBy( - data.results, - ['community.have', 'community.want'], - ['desc', 'desc'], - ) as DiscogsSearchResult[]; + Database.search( + undefined, + { barcode: barcodeValue, type: "release" }, + (err: DiscogsError, data: { results: DiscogsSearchResult[] }) => { + if (err || !data || !data.results || !data.results.length) { + return resolve(undefined); + } + const results = orderBy( + data.results, + ["community.have", "community.want"], + ["desc", "desc"], + ) as DiscogsSearchResult[]; - Cache.set(cacheKey, results[0].id); + Cache.set(cacheKey, results[0].id); - return resolve(results[0].id); - }); + return resolve(results[0].id); + }, + ); }); }); export const search = (query: string): Promise => - new Promise((resolve) => { + new Promise(resolve => { const cacheKey = `search--${query}`; Cache.get(cacheKey) - .then((results) => { + .then(results => { resolve(results); }) .catch(() => { // eslint-disable-next-line consistent-return - Database.search(query, { type: 'release' }, (err: DiscogsError, data: { results: any[] }) => { + Database.search(query, { type: "release" }, (err: DiscogsError, data: { results: any[] }) => { if (err || !data || !data.results || !data.results.length) { return resolve(undefined); } const results: SearchResult[] = data.results.map(result => - pick(result, ['id', 'title', 'thumb', 'country', 'year', 'format', 'uri']), + pick(result, ["id", "title", "thumb", "country", "year", "format", "uri"]), ); Cache.set(cacheKey, results); @@ -174,7 +175,7 @@ export const getRelease = (id: number): Promise => if (err.statusCode === 404) { Database.getMaster(id, (masterErr: DiscogsError, masterData: DiscogsData) => { if (masterErr || !masterData) { - reject(masterErr || new Error('No data returned from Discogs')); + reject(masterErr || new Error("No data returned from Discogs")); } else { resolve(buildRelease(id, masterData)); } @@ -183,7 +184,7 @@ export const getRelease = (id: number): Promise => reject(err); } } else if (!data) { - reject(new Error('No data returned from Discogs')); + reject(new Error("No data returned from Discogs")); } else { resolve(buildRelease(id, data)); } diff --git a/app/lastfm.ts b/app/lastfm.ts index c3c3ce7..5757963 100644 --- a/app/lastfm.ts +++ b/app/lastfm.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-require-imports -const LastFMApi = require('lastfmapi'); +const LastFMApi = require("lastfmapi"); interface ScrobbleTrack { album: string; @@ -28,7 +28,7 @@ const getScrobble = (data: ReleaseForScrobble): ScrobbleTrack[] => { let nextTimestamp = Math.floor(Date.now() / 1000); const trackData: ScrobbleTrack[] = []; - data.tracks.forEach((track) => { + data.tracks.forEach(track => { trackData.push({ album: data.title, artist: data.artist, @@ -44,27 +44,21 @@ const getScrobble = (data: ReleaseForScrobble): ScrobbleTrack[] => { export const scrobbleTracks = (username: string, key: string, data: ReleaseForScrobble): Promise => new Promise((resolve, reject) => { - createApiClient(username, key).track.scrobble( - getScrobble(data), - (err: unknown, scrobbles: unknown) => { - if (err) reject(err); - resolve(scrobbles); - }, - ); + createApiClient(username, key).track.scrobble(getScrobble(data), (err: unknown, scrobbles: unknown) => { + if (err) reject(err); + resolve(scrobbles); + }); }); export interface LastFMUserData { url: string; - image?: Array<{ '#text': string }>; + image?: Array<{ "#text": string }>; } export const getUserData = (username: string, key: string): Promise => new Promise((resolve, reject) => { - createApiClient(username, key).user.getInfo( - null, - (err: unknown, userData: unknown) => { - if (err) reject(err); - resolve(userData as LastFMUserData); - }, - ); + createApiClient(username, key).user.getInfo(null, (err: unknown, userData: unknown) => { + if (err) reject(err); + resolve(userData as LastFMUserData); + }); }); diff --git a/app/models/release.ts b/app/models/release.ts index 4603eab..2408b06 100644 --- a/app/models/release.ts +++ b/app/models/release.ts @@ -1,6 +1,6 @@ -import mongoose, { Schema, HydratedDocument, Model } from 'mongoose'; -import pick from 'lodash/pick'; -import * as Discogs from '../discogs'; +import mongoose, { Schema, HydratedDocument, Model } from "mongoose"; +import pick from "lodash/pick"; +import * as Discogs from "../discogs"; interface ITrack { title: string; @@ -27,7 +27,10 @@ interface IReleaseMethods { interface IReleaseModel extends Model { createFromDiscogs(id: number, barcode?: string): Promise>; - firstOrCreate(param: { id?: string | number; barcode?: string }): Promise | null>; + firstOrCreate(param: { + id?: string | number; + barcode?: string; + }): Promise | null>; } const releaseSchema = new Schema( @@ -41,11 +44,13 @@ const releaseSchema = new Schema( image: String, url: String, year: String, - tracks: [{ - title: String, - trackNumber: Number, - duration: Number, - }], + tracks: [ + { + title: String, + trackNumber: Number, + duration: Number, + }, + ], barcode: String, }, { timestamps: true }, @@ -60,7 +65,7 @@ releaseSchema.methods.toJSON = function toJSON() { image: this.image, url: this.url, year: this.year, - tracks: this.tracks.map((track: ITrack) => pick(track, ['title', 'trackNumber', 'duration'])), + tracks: this.tracks.map((track: ITrack) => pick(track, ["title", "trackNumber", "duration"])), }; }; @@ -82,7 +87,7 @@ releaseSchema.methods.updateFromDiscogs = async function updateFromDiscogs() { }; releaseSchema.statics.createFromDiscogs = async function createFromDiscogs(id: number, barcodeValue?: string) { - const release = new (this)({ id }); + const release = new this({ id }); if (barcodeValue) release.barcode = barcodeValue; await release.updateFromDiscogs(); return release; @@ -92,12 +97,13 @@ releaseSchema.statics.firstOrCreate = async function firstOrCreate(param: { id?: const release = await this.findOne(param).exec(); if (!release) { const paramId = param.id ? Number(param.id) : undefined; - const id = paramId || await Discogs.barcode(param.barcode); + const id = paramId || (await Discogs.barcode(param.barcode)); if (!id) return null; try { return await this.createFromDiscogs(id, param.barcode); - } catch (err: any) { // eslint-disable-line @typescript-eslint/no-explicit-any + } catch (err: any) { + // eslint-disable-line @typescript-eslint/no-explicit-any if (err.code === 11000) { return this.findOne({ id }).exec(); } @@ -113,7 +119,7 @@ releaseSchema.statics.firstOrCreate = async function firstOrCreate(param: { id?: return release; }; -const Release = (mongoose.models.Release as IReleaseModel) || - mongoose.model('Release', releaseSchema); +const Release = + (mongoose.models.Release as IReleaseModel) || mongoose.model("Release", releaseSchema); export default Release; diff --git a/app/models/user.ts b/app/models/user.ts index d0b49cd..34ecfab 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -1,4 +1,4 @@ -import mongoose, { Schema, HydratedDocument, Model } from 'mongoose'; +import mongoose, { Schema, HydratedDocument, Model } from "mongoose"; interface IHistoryItem { id: string; @@ -40,10 +40,12 @@ const userSchema = new Schema({ imageLarge: String, imageXLarge: String, instantScrobbles: [String], - history: [{ - id: String, - time: { type: Date, default: Date.now }, - }], + history: [ + { + id: String, + time: { type: Date, default: Date.now }, + }, + ], }); userSchema.methods.toJSON = function toJSON(): UserJSON { @@ -62,7 +64,8 @@ userSchema.methods.isInstantScrobble = function isInstantScrobble(id: string) { return (this.instantScrobbles || []).includes(String(id)); }; -const User = (mongoose.models.User as UserModel & { new(): HydratedDocument }) || - mongoose.model('User', userSchema); +const User = + (mongoose.models.User as UserModel & { new (): HydratedDocument }) || + mongoose.model("User", userSchema); export default User; diff --git a/app/spec/cache.spec.ts b/app/spec/cache.spec.ts index adf554e..cf6682b 100644 --- a/app/spec/cache.spec.ts +++ b/app/spec/cache.spec.ts @@ -1,23 +1,23 @@ /* eslint-disable no-underscore-dangle */ -import * as Cache from '../cache'; +import * as Cache from "../cache"; // eslint-disable-next-line @typescript-eslint/no-require-imports -const redis = require('redis'); +const redis = require("redis"); -jest.mock('redis'); +jest.mock("redis"); -describe('cache', () => { +describe("cache", () => { beforeEach(() => { redis._get.mockClear(); redis._set.mockClear(); redis._reset(); }); - describe('set', () => { - it('writes given data to redis store', async () => { - const key = 'foo'; - const value = ['bar']; + describe("set", () => { + it("writes given data to redis store", async () => { + const key = "foo"; + const value = ["bar"]; await Cache.set(key, value); @@ -27,9 +27,9 @@ describe('cache', () => { expect(redis._set.mock.calls[0][2]).toEqual({ EX: 86400 }); }); - it('passes given ttl to redis store', async () => { - const key = 'foo'; - const value = ['bar']; + it("passes given ttl to redis store", async () => { + const key = "foo"; + const value = ["bar"]; const ttl = 1337; await Cache.set(key, value, ttl); @@ -37,9 +37,9 @@ describe('cache', () => { expect(redis._set.mock.calls[0][2]).toEqual({ EX: ttl }); }); - it('uses 24 hours as default ttl', async () => { - const key = 'foo'; - const value = ['bar']; + it("uses 24 hours as default ttl", async () => { + const key = "foo"; + const value = ["bar"]; await Cache.set(key, value); @@ -47,13 +47,13 @@ describe('cache', () => { }); }); - describe('get', () => { - it('queries data from redis store', async () => { - const key = 'foo'; + describe("get", () => { + it("queries data from redis store", async () => { + const key = "foo"; try { await Cache.get(key); - } catch (e) { + } catch { // ignore errors } @@ -61,9 +61,9 @@ describe('cache', () => { expect(redis._get.mock.calls[0][0]).toBe(key); }); - it('returns correct data from redis store after it was saved', async () => { - const key = 'foo'; - const value = ['bar']; + it("returns correct data from redis store after it was saved", async () => { + const key = "foo"; + const value = ["bar"]; await Cache.set(key, value); const cachedData = await Cache.get(key); @@ -71,8 +71,8 @@ describe('cache', () => { expect(cachedData).toEqual(value); }); - it('rejects the promise when no data is stored in redis', () => { - const key = 'foo'; + it("rejects the promise when no data is stored in redis", () => { + const key = "foo"; expect(Cache.get(key)).rejects.toBeUndefined(); }); diff --git a/client/reduxStore.ts b/client/reduxStore.ts index d09fd96..1253fb7 100644 --- a/client/reduxStore.ts +++ b/client/reduxStore.ts @@ -1,13 +1,13 @@ -import { thunk as thunkMiddleware } from 'redux-thunk'; -import { combineReducers, createStore, applyMiddleware } from 'redux'; -import { composeWithDevToolsLogOnlyInProduction as composeWithDevTools } from '@redux-devtools/extension'; -import { createWrapper } from 'next-redux-wrapper'; +import { thunk as thunkMiddleware } from "redux-thunk"; +import { combineReducers, createStore, applyMiddleware } from "redux"; +import { composeWithDevToolsLogOnlyInProduction as composeWithDevTools } from "@redux-devtools/extension"; +import { createWrapper } from "next-redux-wrapper"; -import sessionReducer from '../components/session/reducers/sessionReducer'; -import historyReducer from '../components/profile/reducers/historyReducer'; -import autoScrobbleReducer from '../components/profile/reducers/autoScrobbleReducer'; -import releaseReducer from '../components/release/reducers/releaseReducer'; -import queryReducer from '../components/query/reducers/queryReducer'; +import sessionReducer from "../components/session/reducers/sessionReducer"; +import historyReducer from "../components/profile/reducers/historyReducer"; +import autoScrobbleReducer from "../components/profile/reducers/autoScrobbleReducer"; +import releaseReducer from "../components/release/reducers/releaseReducer"; +import queryReducer from "../components/query/reducers/queryReducer"; const reducer = combineReducers({ session: sessionReducer, @@ -17,10 +17,7 @@ const reducer = combineReducers({ query: queryReducer, }); -const makeStore = () => createStore( - reducer, - composeWithDevTools(applyMiddleware(thunkMiddleware)), -); +const makeStore = () => createStore(reducer, composeWithDevTools(applyMiddleware(thunkMiddleware))); export const wrapper = createWrapper(makeStore); export default makeStore; diff --git a/components/assets/Logo.tsx b/components/assets/Logo.tsx index 2368ff8..0d33585 100644 --- a/components/assets/Logo.tsx +++ b/components/assets/Logo.tsx @@ -1,7 +1,10 @@ const Logo = ({ className = undefined, ...props }: { className?: string; [key: string]: any }) => ( CodeScrobble - + diff --git a/components/assets/LogoSmall.tsx b/components/assets/LogoSmall.tsx index 7b8ae2e..8b0e0e4 100644 --- a/components/assets/LogoSmall.tsx +++ b/components/assets/LogoSmall.tsx @@ -1,7 +1,10 @@ const Logo = ({ className = undefined, ...props }: { className?: string; [key: string]: any }) => ( CodeScrobble - + ); diff --git a/components/icons/ErrorIcon.tsx b/components/icons/ErrorIcon.tsx index 8490401..8e7bc0d 100644 --- a/components/icons/ErrorIcon.tsx +++ b/components/icons/ErrorIcon.tsx @@ -4,7 +4,7 @@ interface ErrorIconProps { className?: string; } -const ErrorIcon = ({ color = '#000', size = 100, className }: ErrorIconProps) => ( +const ErrorIcon = ({ color = "#000", size = 100, className }: ErrorIconProps) => ( Error diff --git a/components/icons/LastfmIcon.tsx b/components/icons/LastfmIcon.tsx index b44f0d6..3db2d1f 100644 --- a/components/icons/LastfmIcon.tsx +++ b/components/icons/LastfmIcon.tsx @@ -4,10 +4,13 @@ interface LastfmIconProps { [key: string]: any; } -const LastfmIcon = ({ color = '#fff', ...props }: LastfmIconProps) => ( +const LastfmIcon = ({ color = "#fff", ...props }: LastfmIconProps) => ( Last.fm - + ); diff --git a/components/icons/LogoIcon.tsx b/components/icons/LogoIcon.tsx index 26c8648..d310102 100644 --- a/components/icons/LogoIcon.tsx +++ b/components/icons/LogoIcon.tsx @@ -4,7 +4,7 @@ interface LogoIconProps { className?: string; } -const LogoIcon = ({ color = '#000', size = 100, className }: LogoIconProps) => ( +const LogoIcon = ({ color = "#000", size = 100, className }: LogoIconProps) => ( diff --git a/components/icons/NoResultsIcon.tsx b/components/icons/NoResultsIcon.tsx index e798c4a..d3d202f 100644 --- a/components/icons/NoResultsIcon.tsx +++ b/components/icons/NoResultsIcon.tsx @@ -4,7 +4,7 @@ interface NoResultsIconProps { className?: string; } -const NoResultsIcon = ({ color = '#000', size = 100, className }: NoResultsIconProps) => ( +const NoResultsIcon = ({ color = "#000", size = 100, className }: NoResultsIconProps) => ( diff --git a/components/layout/BaseStyles.ts b/components/layout/BaseStyles.ts index e896885..2d99977 100644 --- a/components/layout/BaseStyles.ts +++ b/components/layout/BaseStyles.ts @@ -1,5 +1,5 @@ -import { createGlobalStyle } from 'styled-components'; -import { dark, silver } from '../../lib/colors'; +import { createGlobalStyle } from "styled-components"; +import { dark, silver } from "../../lib/colors"; export default createGlobalStyle` html { diff --git a/components/layout/CircleLayout.tsx b/components/layout/CircleLayout.tsx index b907d33..b61130a 100644 --- a/components/layout/CircleLayout.tsx +++ b/components/layout/CircleLayout.tsx @@ -1,11 +1,19 @@ -import React from 'react'; -import { createGlobalStyle } from 'styled-components'; -import Link from 'next/link'; +import React from "react"; +import { createGlobalStyle } from "styled-components"; +import Link from "next/link"; import { - Center, Content, Footer, Header, HeightWrapper, Logo, LogoWrapper, SessionWrapper, Wrapper, -} from '../../styles/layout.styles'; -import Session from '../session/Session'; -import LegalLinks from '../ui/LegalLinks'; + Center, + Content, + Footer, + Header, + HeightWrapper, + Logo, + LogoWrapper, + SessionWrapper, + Wrapper, +} from "../../styles/layout.styles"; +import Session from "../session/Session"; +import LegalLinks from "../ui/LegalLinks"; const ScrollLock = createGlobalStyle` body { @@ -37,9 +45,7 @@ const CircleLayout = ({ children = null, header = null, footer = null }: CircleL {header} - - {children} - + {children}
diff --git a/components/layout/Loading.tsx b/components/layout/Loading.tsx index cdad63d..3874a95 100644 --- a/components/layout/Loading.tsx +++ b/components/layout/Loading.tsx @@ -1,4 +1,4 @@ -import { LoadingSpinner, LoadingWrapper } from './styles/Loading.styles'; +import { LoadingSpinner, LoadingWrapper } from "./styles/Loading.styles"; const Loading = () => ( diff --git a/components/layout/Spinner.tsx b/components/layout/Spinner.tsx index ff026be..c6f3aa6 100644 --- a/components/layout/Spinner.tsx +++ b/components/layout/Spinner.tsx @@ -1,5 +1,5 @@ -import styled, { keyframes } from 'styled-components'; -import { yellow, yellowRGB } from '../../lib/colors'; +import styled, { keyframes } from "styled-components"; +import { yellow, yellowRGB } from "../../lib/colors"; export const animation = keyframes` to { @@ -12,7 +12,7 @@ const Spinner = styled.div<{ size?: number | string }>` width: ${props => props.size}px; height: ${props => props.size}px; animation: ${animation} 1s ease-in-out infinite; - border: 3px solid rgba(${yellowRGB}, .3); + border: 3px solid rgba(${yellowRGB}, 0.3); border-radius: 50%; border-top-color: ${yellow}; `; diff --git a/components/layout/styles/Error.styles.ts b/components/layout/styles/Error.styles.ts index 7d01e8c..819bace 100644 --- a/components/layout/styles/Error.styles.ts +++ b/components/layout/styles/Error.styles.ts @@ -1,5 +1,5 @@ -import styled from 'styled-components'; -import ErrorIconSVG from '../../icons/ErrorIcon'; +import styled from "styled-components"; +import ErrorIconSVG from "../../icons/ErrorIcon"; export const Error = styled.div` display: flex; diff --git a/components/layout/styles/Loading.styles.ts b/components/layout/styles/Loading.styles.ts index e06e205..86891ef 100644 --- a/components/layout/styles/Loading.styles.ts +++ b/components/layout/styles/Loading.styles.ts @@ -1,5 +1,5 @@ -import styled from 'styled-components'; -import Spinner from '../Spinner'; +import styled from "styled-components"; +import Spinner from "../Spinner"; export const LoadingWrapper = styled.div` display: flex; diff --git a/components/profile/ProfileAutoScrobbleItem.tsx b/components/profile/ProfileAutoScrobbleItem.tsx index 3a35482..f692315 100644 --- a/components/profile/ProfileAutoScrobbleItem.tsx +++ b/components/profile/ProfileAutoScrobbleItem.tsx @@ -1,9 +1,9 @@ -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import React from 'react'; -import { TrashAlt as DeleteIcon } from 'styled-icons/fa-regular'; -import { deleteAutoScrobble } from './actions/autoScrobbleActions'; -import { DeleteButton, ListCaption, ListItem } from '../../styles/profile.styles'; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import React from "react"; +import { TrashAlt as DeleteIcon } from "styled-icons/fa-regular"; +import { deleteAutoScrobble } from "./actions/autoScrobbleActions"; +import { DeleteButton, ListCaption, ListItem } from "../../styles/profile.styles"; interface ProfileAutoScrobbleItemProps { id: string; @@ -18,12 +18,10 @@ class ProfileAutoScrobbleItem extends React.PureComponent { const { id } = this.props; this.props.deleteAutoScrobble(id); - } + }; render() { - const { - id, artist, title, year, isDeleting = false, - } = this.props; + const { id, artist, title, year, isDeleting = false } = this.props; return ( @@ -38,11 +36,6 @@ class ProfileAutoScrobbleItem extends React.PureComponent ( - bindActionCreators({ deleteAutoScrobble }, dispatch) -); +const mapDispatchToProps = dispatch => bindActionCreators({ deleteAutoScrobble }, dispatch); -export default connect( - null, - mapDispatchToProps, -)(ProfileAutoScrobbleItem); +export default connect(null, mapDispatchToProps)(ProfileAutoScrobbleItem); diff --git a/components/profile/ProfileAutoScrobbles.tsx b/components/profile/ProfileAutoScrobbles.tsx index e1f025d..e0cae5f 100644 --- a/components/profile/ProfileAutoScrobbles.tsx +++ b/components/profile/ProfileAutoScrobbles.tsx @@ -1,12 +1,10 @@ -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import React from 'react'; -import { fetchAutoScrobbles } from './actions/autoScrobbleActions'; -import { - Fallback, H3, List, Meta, -} from '../../styles/profile.styles'; -import ProfileAutoScrobbleItem from './ProfileAutoScrobbleItem'; -import Spinner from '../layout/Spinner'; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import React from "react"; +import { fetchAutoScrobbles } from "./actions/autoScrobbleActions"; +import { Fallback, H3, List, Meta } from "../../styles/profile.styles"; +import ProfileAutoScrobbleItem from "./ProfileAutoScrobbleItem"; +import Spinner from "../layout/Spinner"; interface ProfileAutoScrobblesProps { data?: any[]; @@ -21,33 +19,25 @@ class ProfileAutoScrobbles extends React.PureComponent

Auto-scrobbles

These items are automatically scrobbled the next time they are scanned - {loading - ? - : ( - - {!data.length && ( - - No entries found. Activate the option "Auto-scrobble" - during your next scan. - - )} - {data.map(item => ( - - ))} - - ) - } + {loading ? ( + + ) : ( + + {!data.length && ( + + No entries found. Activate the option "Auto-scrobble" during your next scan. + + )} + {data.map(item => ( + + ))} + + )} ); } @@ -59,11 +49,6 @@ const mapStateToProps = state => ({ deleting: state.autoScrobbles.deleting, }); -const mapDispatchToProps = dispatch => ( - bindActionCreators({ fetchAutoScrobbles }, dispatch) -); +const mapDispatchToProps = dispatch => bindActionCreators({ fetchAutoScrobbles }, dispatch); -export default connect( - mapStateToProps, - mapDispatchToProps, -)(ProfileAutoScrobbles); +export default connect(mapStateToProps, mapDispatchToProps)(ProfileAutoScrobbles); diff --git a/components/profile/ProfileHistory.tsx b/components/profile/ProfileHistory.tsx index 857ea6d..0189f0e 100644 --- a/components/profile/ProfileHistory.tsx +++ b/components/profile/ProfileHistory.tsx @@ -1,12 +1,10 @@ -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import React from 'react'; -import { fetchHistory } from './actions/historyActions'; -import { - Fallback, H3, List, Meta, -} from '../../styles/profile.styles'; -import ProfileHistoryItem from './ProfileHistoryItem'; -import Spinner from '../layout/Spinner'; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import React from "react"; +import { fetchHistory } from "./actions/historyActions"; +import { Fallback, H3, List, Meta } from "../../styles/profile.styles"; +import ProfileHistoryItem from "./ProfileHistoryItem"; +import Spinner from "../layout/Spinner"; interface ProfileHistoryProps { history?: any[]; @@ -25,24 +23,16 @@ class ProfileHistory extends React.PureComponent { <>

History

Your recently scanned items. Tap one to scrobble it again. - {loading - ? - : ( - - {!history.length && ( - - No entries found. - - )} - {history.map(item => ( - - ))} - - ) - } + {loading ? ( + + ) : ( + + {!history.length && No entries found.} + {history.map(item => ( + + ))} + + )} ); } @@ -53,11 +43,6 @@ const mapStateToProps = state => ({ loading: state.history.loading, }); -const mapDispatchToProps = dispatch => ( - bindActionCreators({ fetchHistory }, dispatch) -); +const mapDispatchToProps = dispatch => bindActionCreators({ fetchHistory }, dispatch); -export default connect( - mapStateToProps, - mapDispatchToProps, -)(ProfileHistory); +export default connect(mapStateToProps, mapDispatchToProps)(ProfileHistory); diff --git a/components/profile/ProfileHistoryItem.tsx b/components/profile/ProfileHistoryItem.tsx index 4983271..6a12026 100644 --- a/components/profile/ProfileHistoryItem.tsx +++ b/components/profile/ProfileHistoryItem.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import Link from 'next/link'; -import { ListCaption, ListItem, Time } from '../../styles/profile.styles'; +import React from "react"; +import Link from "next/link"; +import { ListCaption, ListItem, Time } from "../../styles/profile.styles"; interface ProfileHistoryItemProps { id: string; @@ -15,9 +15,7 @@ interface ProfileHistoryItemProps { class ProfileHistoryItem extends React.PureComponent { render() { - const { - id, artist, title, year, barcode, discogsId, time, isDeleting = false, - } = this.props; + const { id, artist, title, year, barcode, discogsId, time, isDeleting = false } = this.props; const barcodeParam = barcode || `id:${discogsId}`; diff --git a/components/profile/actions/autoScrobbleActionCreators.ts b/components/profile/actions/autoScrobbleActionCreators.ts index 20f27f1..9962faa 100644 --- a/components/profile/actions/autoScrobbleActionCreators.ts +++ b/components/profile/actions/autoScrobbleActionCreators.ts @@ -1,7 +1,11 @@ import { - SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_AUTO_SCROBBLES, START_DELETING, - REMOVE_AUTO_SCROBBLE, END_DELETING, -} from '../constants/autoScrobbleConstants'; + SET_LOADING_STATE, + SET_ERROR_STATE, + RECEIVED_AUTO_SCROBBLES, + START_DELETING, + REMOVE_AUTO_SCROBBLE, + END_DELETING, +} from "../constants/autoScrobbleConstants"; export const setLoadingState = loading => ({ type: SET_LOADING_STATE, diff --git a/components/profile/actions/autoScrobbleActions.ts b/components/profile/actions/autoScrobbleActions.ts index 96e33b5..0a0fdb8 100644 --- a/components/profile/actions/autoScrobbleActions.ts +++ b/components/profile/actions/autoScrobbleActions.ts @@ -1,37 +1,37 @@ import { - setLoadingState, receivedAutoScrobbles, setErrorState, startDeleting, - removeAutoScrobble, endDeleting, -} from './autoScrobbleActionCreators'; + setLoadingState, + receivedAutoScrobbles, + setErrorState, + startDeleting, + removeAutoScrobble, + endDeleting, +} from "./autoScrobbleActionCreators"; -export const fetchAutoScrobbles = () => ( - async (dispatch) => { - try { - dispatch(setLoadingState(true)); - const response = await fetch('/api/user/autoscrobbles', { credentials: 'include' }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const data = await response.json(); - dispatch(receivedAutoScrobbles(data)); - } catch (error) { - dispatch(setErrorState(error)); - } - dispatch(setLoadingState(false)); +export const fetchAutoScrobbles = () => async dispatch => { + try { + dispatch(setLoadingState(true)); + const response = await fetch("/api/user/autoscrobbles", { credentials: "include" }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + dispatch(receivedAutoScrobbles(data)); + } catch (error) { + dispatch(setErrorState(error)); } -); + dispatch(setLoadingState(false)); +}; -export const deleteAutoScrobble = id => ( - async (dispatch) => { - try { - dispatch(startDeleting(id)); - await fetch('/api/user/autoscrobbles', { - method: 'DELETE', - credentials: 'include', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id }), - }); - dispatch(removeAutoScrobble(id)); - } catch (error) { - dispatch(setErrorState(error)); - } - dispatch(endDeleting(id)); +export const deleteAutoScrobble = id => async dispatch => { + try { + dispatch(startDeleting(id)); + await fetch("/api/user/autoscrobbles", { + method: "DELETE", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id }), + }); + dispatch(removeAutoScrobble(id)); + } catch (error) { + dispatch(setErrorState(error)); } -); + dispatch(endDeleting(id)); +}; diff --git a/components/profile/actions/historyActionCreators.ts b/components/profile/actions/historyActionCreators.ts index 503fed4..d346f72 100644 --- a/components/profile/actions/historyActionCreators.ts +++ b/components/profile/actions/historyActionCreators.ts @@ -1,4 +1,4 @@ -import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_HISTORY } from '../constants/historyConstants'; +import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_HISTORY } from "../constants/historyConstants"; export const setLoadingState = loading => ({ type: SET_LOADING_STATE, diff --git a/components/profile/actions/historyActions.ts b/components/profile/actions/historyActions.ts index 6acaa76..1ac0604 100644 --- a/components/profile/actions/historyActions.ts +++ b/components/profile/actions/historyActions.ts @@ -1,17 +1,15 @@ -import { setLoadingState, receivedHistory, setErrorState } from './historyActionCreators'; +import { setLoadingState, receivedHistory, setErrorState } from "./historyActionCreators"; // eslint-disable-next-line import/prefer-default-export -export const fetchHistory = () => ( - async (dispatch) => { - try { - dispatch(setLoadingState(true)); - const response = await fetch('/api/user/history', { credentials: 'include' }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const data = await response.json(); - dispatch(receivedHistory(data)); - } catch (error) { - dispatch(setErrorState(error)); - } - dispatch(setLoadingState(false)); +export const fetchHistory = () => async dispatch => { + try { + dispatch(setLoadingState(true)); + const response = await fetch("/api/user/history", { credentials: "include" }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + dispatch(receivedHistory(data)); + } catch (error) { + dispatch(setErrorState(error)); } -); + dispatch(setLoadingState(false)); +}; diff --git a/components/profile/actions/spec/autoScrobbleActionCreators.spec.ts b/components/profile/actions/spec/autoScrobbleActionCreators.spec.ts index 3886d96..30ba317 100644 --- a/components/profile/actions/spec/autoScrobbleActionCreators.spec.ts +++ b/components/profile/actions/spec/autoScrobbleActionCreators.spec.ts @@ -1,86 +1,94 @@ import { - setLoadingState, setErrorState, receivedAutoScrobbles, startDeleting, removeAutoScrobble, + setLoadingState, + setErrorState, + receivedAutoScrobbles, + startDeleting, + removeAutoScrobble, endDeleting, -} from '../autoScrobbleActionCreators'; +} from "../autoScrobbleActionCreators"; import { - SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_AUTO_SCROBBLES, START_DELETING, END_DELETING, + SET_LOADING_STATE, + SET_ERROR_STATE, + RECEIVED_AUTO_SCROBBLES, + START_DELETING, + END_DELETING, REMOVE_AUTO_SCROBBLE, -} from '../../constants/autoScrobbleConstants'; +} from "../../constants/autoScrobbleConstants"; -describe('autoScrobbleActionCreators', () => { - describe('setLoadingState', () => { +describe("autoScrobbleActionCreators", () => { + describe("setLoadingState", () => { const setLoadingStateAction = setLoadingState(true); - it('returns correct type', () => { + it("returns correct type", () => { expect(setLoadingStateAction).toMatchObject({ type: SET_LOADING_STATE }); }); - it('returns passed loading state in action', () => { + it("returns passed loading state in action", () => { expect(setLoadingStateAction).toMatchObject({ loading: true }); }); }); - describe('setErrorState', () => { - const error = 'FooBar'; + describe("setErrorState", () => { + const error = "FooBar"; const setErrorStateAction = setErrorState(error); - it('returns correct type', () => { + it("returns correct type", () => { expect(setErrorStateAction).toMatchObject({ type: SET_ERROR_STATE }); }); - it('returns passed error state in action', () => { + it("returns passed error state in action", () => { expect(setErrorStateAction).toMatchObject({ error }); }); }); - describe('receivedAutoScrobbles', () => { - const autoScrobbles = ['foo', 'bar']; + describe("receivedAutoScrobbles", () => { + const autoScrobbles = ["foo", "bar"]; const receivedAutoScrobblesAction = receivedAutoScrobbles(autoScrobbles); - it('returns correct type', () => { + it("returns correct type", () => { expect(receivedAutoScrobblesAction).toMatchObject({ type: RECEIVED_AUTO_SCROBBLES }); }); - it('returns autoScrobbles in action', () => { + it("returns autoScrobbles in action", () => { expect(receivedAutoScrobblesAction).toMatchObject({ autoScrobbles }); }); }); - describe('startDeleting', () => { + describe("startDeleting", () => { const id = 1337; const startDeletingAction = startDeleting(id); - it('returns correct type', () => { + it("returns correct type", () => { expect(startDeletingAction).toMatchObject({ type: START_DELETING }); }); - it('returns id in action', () => { + it("returns id in action", () => { expect(startDeletingAction).toMatchObject({ id }); }); }); - describe('removeAutoScrobble', () => { + describe("removeAutoScrobble", () => { const id = 1337; const removeAutoScrobbleAction = removeAutoScrobble(id); - it('returns correct type', () => { + it("returns correct type", () => { expect(removeAutoScrobbleAction).toMatchObject({ type: REMOVE_AUTO_SCROBBLE }); }); - it('returns id in action', () => { + it("returns id in action", () => { expect(removeAutoScrobbleAction).toMatchObject({ id }); }); }); - describe('endDeleting', () => { + describe("endDeleting", () => { const id = 1337; const endDeletingAction = endDeleting(id); - it('returns correct type', () => { + it("returns correct type", () => { expect(endDeletingAction).toMatchObject({ type: END_DELETING }); }); - it('returns id in action', () => { + it("returns id in action", () => { expect(endDeletingAction).toMatchObject({ id }); }); }); diff --git a/components/profile/actions/spec/autoScrobbleActions.spec.ts b/components/profile/actions/spec/autoScrobbleActions.spec.ts index 0ad5577..4d68c33 100644 --- a/components/profile/actions/spec/autoScrobbleActions.spec.ts +++ b/components/profile/actions/spec/autoScrobbleActions.spec.ts @@ -1,10 +1,8 @@ -import configureMockStore from 'redux-mock-store'; -import { thunk } from "redux-thunk";; +import configureMockStore from "redux-mock-store"; +import { thunk } from "redux-thunk"; -import * as actionCreators from '../autoScrobbleActionCreators'; -import { - fetchAutoScrobbles, deleteAutoScrobble, -} from '../autoScrobbleActions'; +import * as actionCreators from "../autoScrobbleActionCreators"; +import { fetchAutoScrobbles, deleteAutoScrobble } from "../autoScrobbleActions"; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); @@ -13,14 +11,14 @@ const emptyStore = () => mockStore(); const demoAutoScrobbleData = [ { - id: '5ccb6ad74c5f76adff751342', - artist: 'Farin Urlaub', - title: 'Am Ende Der Sonne', - year: '2005', + id: "5ccb6ad74c5f76adff751342", + artist: "Farin Urlaub", + title: "Am Ende Der Sonne", + year: "2005", }, ]; -describe('historyActions', () => { +describe("historyActions", () => { beforeEach(() => { fetch.mockResponse(JSON.stringify(demoAutoScrobbleData)); }); @@ -28,115 +26,117 @@ describe('historyActions', () => { fetch.resetMocks(); }); - describe('fetchAutoScrobbles', () => { - it('sets loading state to true as first action', () => { + describe("fetchAutoScrobbles", () => { + it("sets loading state to true as first action", () => { const expectedAction = actionCreators.setLoadingState(true); const store = emptyStore(); - return store.dispatch(fetchAutoScrobbles()) - .then(() => expect(store.getActions()[0]).toEqual(expectedAction)); + return store.dispatch(fetchAutoScrobbles()).then(() => expect(store.getActions()[0]).toEqual(expectedAction)); }); - it('sets loading state back to false as last action', () => { + it("sets loading state back to false as last action", () => { const expectedAction = actionCreators.setLoadingState(false); const store = emptyStore(); - return store.dispatch(fetchAutoScrobbles()) + return store + .dispatch(fetchAutoScrobbles()) .then(() => expect(store.getActions().slice(-1)[0]).toEqual(expectedAction)); }); - it('creates RECEIVED_HISTORY when fetching history has been done', () => { + it("creates RECEIVED_HISTORY when fetching history has been done", () => { const expectedAction = actionCreators.receivedAutoScrobbles(demoAutoScrobbleData); const store = emptyStore(); - return store.dispatch(fetchAutoScrobbles()) - .then(() => expect(store.getActions()).toContainEqual(expectedAction)); + return store.dispatch(fetchAutoScrobbles()).then(() => expect(store.getActions()).toContainEqual(expectedAction)); }); - it('sets error state to store when loading fails', () => { - const error = new Error('Foooo!'); + it("sets error state to store when loading fails", () => { + const error = new Error("Foooo!"); fetch.mockReject(error); const expectedAction = actionCreators.setErrorState(error); const store = emptyStore(); - return store.dispatch(fetchAutoScrobbles()) - .then(() => expect(store.getActions()).toContainEqual(expectedAction)); + return store.dispatch(fetchAutoScrobbles()).then(() => expect(store.getActions()).toContainEqual(expectedAction)); }); - it('sets loading state back to false after an error occured', () => { + it("sets loading state back to false after an error occured", () => { const expectedAction = actionCreators.setLoadingState(false); const store = emptyStore(); - return store.dispatch(fetchAutoScrobbles()) + return store + .dispatch(fetchAutoScrobbles()) .then(() => expect(store.getActions().slice(-1)[0]).toEqual(expectedAction)); }); - it('sends a GET request to the api to get the history data', () => { + it("sends a GET request to the api to get the history data", () => { const store = emptyStore(); return store.dispatch(fetchAutoScrobbles()).then(() => { expect(fetch.mock.calls.length).toEqual(1); - expect(fetch.mock.calls[0][0]).toEqual('/api/user/autoscrobbles'); + expect(fetch.mock.calls[0][0]).toEqual("/api/user/autoscrobbles"); }); }); }); - describe('deleteAutoScrobble', () => { - it('adds given id to deleting array as first action', () => { + describe("deleteAutoScrobble", () => { + it("adds given id to deleting array as first action", () => { const id = 1337; const expectedAction = actionCreators.startDeleting(id); const store = emptyStore(); - return store.dispatch(deleteAutoScrobble(id)) - .then(() => expect(store.getActions()[0]).toEqual(expectedAction)); + return store.dispatch(deleteAutoScrobble(id)).then(() => expect(store.getActions()[0]).toEqual(expectedAction)); }); - it('removes given id from deleting array as last action', () => { + it("removes given id from deleting array as last action", () => { const id = 1337; const expectedAction = actionCreators.endDeleting(id); const store = emptyStore(); - return store.dispatch(deleteAutoScrobble(id)) + return store + .dispatch(deleteAutoScrobble(id)) .then(() => expect(store.getActions().slice(-1)[0]).toEqual(expectedAction)); }); - it('removes the given id from autoscrobbles after deleting', () => { + it("removes the given id from autoscrobbles after deleting", () => { const id = 1337; const expectedAction = actionCreators.removeAutoScrobble(id); const store = emptyStore(); - return store.dispatch(deleteAutoScrobble(id)) + return store + .dispatch(deleteAutoScrobble(id)) .then(() => expect(store.getActions()).toContainEqual(expectedAction)); }); - it('sets error state to store when loading fails', () => { + it("sets error state to store when loading fails", () => { const id = 1337; - const error = new Error('Foooo!'); + const error = new Error("Foooo!"); fetch.mockReject(error); const expectedAction = actionCreators.setErrorState(error); const store = emptyStore(); - return store.dispatch(deleteAutoScrobble(id)) + return store + .dispatch(deleteAutoScrobble(id)) .then(() => expect(store.getActions()).toContainEqual(expectedAction)); }); - it('sets loading state back to false after an error occured', () => { + it("sets loading state back to false after an error occured", () => { const id = 1337; const expectedAction = actionCreators.endDeleting(id); const store = emptyStore(); - return store.dispatch(deleteAutoScrobble(id)) + return store + .dispatch(deleteAutoScrobble(id)) .then(() => expect(store.getActions().slice(-1)[0]).toEqual(expectedAction)); }); - it('sends a DELETE request to the api to delete the data', () => { + it("sends a DELETE request to the api to delete the data", () => { const id = 1337; const store = emptyStore(); return store.dispatch(deleteAutoScrobble(id)).then(() => { expect(fetch.mock.calls.length).toEqual(1); - expect(fetch.mock.calls[0][0]).toEqual('/api/user/autoscrobbles'); - expect(fetch.mock.calls[0][1].method).toEqual('DELETE'); + expect(fetch.mock.calls[0][0]).toEqual("/api/user/autoscrobbles"); + expect(fetch.mock.calls[0][1].method).toEqual("DELETE"); expect(fetch.mock.calls[0][1].body).toEqual(JSON.stringify({ id })); }); }); diff --git a/components/profile/actions/spec/historyActionCreators.spec.ts b/components/profile/actions/spec/historyActionCreators.spec.ts index eaad1c2..c28c314 100644 --- a/components/profile/actions/spec/historyActionCreators.spec.ts +++ b/components/profile/actions/spec/historyActionCreators.spec.ts @@ -1,41 +1,41 @@ -import { setLoadingState, setErrorState, receivedHistory } from '../historyActionCreators'; -import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_HISTORY } from '../../constants/historyConstants'; +import { setLoadingState, setErrorState, receivedHistory } from "../historyActionCreators"; +import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_HISTORY } from "../../constants/historyConstants"; -describe('historyActionCreators', () => { - describe('setLoadingState', () => { +describe("historyActionCreators", () => { + describe("setLoadingState", () => { const setLoadingStateAction = setLoadingState(true); - it('returns correct type', () => { + it("returns correct type", () => { expect(setLoadingStateAction).toMatchObject({ type: SET_LOADING_STATE }); }); - it('returns passed loading state in action', () => { + it("returns passed loading state in action", () => { expect(setLoadingStateAction).toMatchObject({ loading: true }); }); }); - describe('setErrorState', () => { - const error = 'FooBar'; + describe("setErrorState", () => { + const error = "FooBar"; const setErrorStateAction = setErrorState(error); - it('returns correct type', () => { + it("returns correct type", () => { expect(setErrorStateAction).toMatchObject({ type: SET_ERROR_STATE }); }); - it('returns passed error state in action', () => { + it("returns passed error state in action", () => { expect(setErrorStateAction).toMatchObject({ error }); }); }); - describe('receivedHistory', () => { - const history = ['foo', 'bar']; + describe("receivedHistory", () => { + const history = ["foo", "bar"]; const receivedHistoryAction = receivedHistory(history); - it('returns correct type', () => { + it("returns correct type", () => { expect(receivedHistoryAction).toMatchObject({ type: RECEIVED_HISTORY }); }); - it('returns history in action', () => { + it("returns history in action", () => { expect(receivedHistoryAction).toMatchObject({ history }); }); }); diff --git a/components/profile/actions/spec/historyActions.spec.ts b/components/profile/actions/spec/historyActions.spec.ts index a8bbb5a..99eeb1d 100644 --- a/components/profile/actions/spec/historyActions.spec.ts +++ b/components/profile/actions/spec/historyActions.spec.ts @@ -1,25 +1,27 @@ -import configureMockStore from 'redux-mock-store'; -import { thunk } from "redux-thunk";; +import configureMockStore from "redux-mock-store"; +import { thunk } from "redux-thunk"; -import * as actionCreators from '../historyActionCreators'; -import { fetchHistory } from '../historyActions'; +import * as actionCreators from "../historyActionCreators"; +import { fetchHistory } from "../historyActions"; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); const emptyStore = () => mockStore({}); -const demoHistoryData = [{ - id: '5ccb6ae44c5f76adff751352', - time: '2019-05-02T22:10:44.378Z', - artist: 'Farin Urlaub', - title: 'Am Ende Der Sonne', - year: '2005', - barcode: '0419594000028', - discogsId: 2852926, -}]; - -describe('historyActions', () => { +const demoHistoryData = [ + { + id: "5ccb6ae44c5f76adff751352", + time: "2019-05-02T22:10:44.378Z", + artist: "Farin Urlaub", + title: "Am Ende Der Sonne", + year: "2005", + barcode: "0419594000028", + discogsId: 2852926, + }, +]; + +describe("historyActions", () => { beforeEach(() => { fetch.mockResponse(JSON.stringify(demoHistoryData)); }); @@ -27,54 +29,49 @@ describe('historyActions', () => { fetch.resetMocks(); }); - it('sets loading state to true as first action', () => { + it("sets loading state to true as first action", () => { const expectedAction = actionCreators.setLoadingState(true); const store = emptyStore(); - return store.dispatch(fetchHistory()) - .then(() => expect(store.getActions()[0]).toEqual(expectedAction)); + return store.dispatch(fetchHistory()).then(() => expect(store.getActions()[0]).toEqual(expectedAction)); }); - it('sets loading state back to false as last action', () => { + it("sets loading state back to false as last action", () => { const expectedAction = actionCreators.setLoadingState(false); const store = emptyStore(); - return store.dispatch(fetchHistory()) - .then(() => expect(store.getActions().slice(-1)[0]).toEqual(expectedAction)); + return store.dispatch(fetchHistory()).then(() => expect(store.getActions().slice(-1)[0]).toEqual(expectedAction)); }); - it('creates RECEIVED_HISTORY when fetching history has been done', () => { + it("creates RECEIVED_HISTORY when fetching history has been done", () => { const expectedAction = actionCreators.receivedHistory(demoHistoryData); const store = emptyStore(); - return store.dispatch(fetchHistory()) - .then(() => expect(store.getActions()).toContainEqual(expectedAction)); + return store.dispatch(fetchHistory()).then(() => expect(store.getActions()).toContainEqual(expectedAction)); }); - it('sets error state to store when loading fails', () => { - const error = new Error('Foooo!'); + it("sets error state to store when loading fails", () => { + const error = new Error("Foooo!"); fetch.mockReject(error); const expectedAction = actionCreators.setErrorState(error); const store = emptyStore(); - return store.dispatch(fetchHistory()) - .then(() => expect(store.getActions()).toContainEqual(expectedAction)); + return store.dispatch(fetchHistory()).then(() => expect(store.getActions()).toContainEqual(expectedAction)); }); - it('sets loading state back to false after an error occured', () => { + it("sets loading state back to false after an error occured", () => { const expectedAction = actionCreators.setLoadingState(false); const store = emptyStore(); - return store.dispatch(fetchHistory()) - .then(() => expect(store.getActions().slice(-1)[0]).toEqual(expectedAction)); + return store.dispatch(fetchHistory()).then(() => expect(store.getActions().slice(-1)[0]).toEqual(expectedAction)); }); - it('sends a GET request to the api to get the history data', () => { + it("sends a GET request to the api to get the history data", () => { const store = emptyStore(); return store.dispatch(fetchHistory()).then(() => { expect(fetch.mock.calls.length).toEqual(1); - expect(fetch.mock.calls[0][0]).toEqual('/api/user/history'); + expect(fetch.mock.calls[0][0]).toEqual("/api/user/history"); }); }); }); diff --git a/components/profile/constants/autoScrobbleConstants.ts b/components/profile/constants/autoScrobbleConstants.ts index 95c537c..1dce47d 100644 --- a/components/profile/constants/autoScrobbleConstants.ts +++ b/components/profile/constants/autoScrobbleConstants.ts @@ -1,6 +1,6 @@ -export const SET_LOADING_STATE = 'SET_AUTO_SCROBBLES_LOADING_STATE'; -export const SET_ERROR_STATE = 'SET_AUTO_SCROBBLES_ERROR_STATE'; -export const RECEIVED_AUTO_SCROBBLES = 'RECEIVED_AUTO_SCROBBLES'; -export const START_DELETING = 'START_DELETING_AUTO_SCROBBLE'; -export const END_DELETING = 'END_DELETING_AUTO_SCROBBLE'; -export const REMOVE_AUTO_SCROBBLE = 'REMOVE_AUTO_SCROBBLE'; +export const SET_LOADING_STATE = "SET_AUTO_SCROBBLES_LOADING_STATE"; +export const SET_ERROR_STATE = "SET_AUTO_SCROBBLES_ERROR_STATE"; +export const RECEIVED_AUTO_SCROBBLES = "RECEIVED_AUTO_SCROBBLES"; +export const START_DELETING = "START_DELETING_AUTO_SCROBBLE"; +export const END_DELETING = "END_DELETING_AUTO_SCROBBLE"; +export const REMOVE_AUTO_SCROBBLE = "REMOVE_AUTO_SCROBBLE"; diff --git a/components/profile/constants/historyConstants.ts b/components/profile/constants/historyConstants.ts index fdf1c6b..bf7c9a6 100644 --- a/components/profile/constants/historyConstants.ts +++ b/components/profile/constants/historyConstants.ts @@ -1,3 +1,3 @@ -export const SET_LOADING_STATE = 'SET_HISTORY_LOADING_STATE'; -export const SET_ERROR_STATE = 'SET_HISTORY_ERROR_STATE'; -export const RECEIVED_HISTORY = 'RECEIVED_HISTORY'; +export const SET_LOADING_STATE = "SET_HISTORY_LOADING_STATE"; +export const SET_ERROR_STATE = "SET_HISTORY_ERROR_STATE"; +export const RECEIVED_HISTORY = "RECEIVED_HISTORY"; diff --git a/components/profile/reducers/autoScrobbleReducer.ts b/components/profile/reducers/autoScrobbleReducer.ts index 397670a..4bf93ce 100644 --- a/components/profile/reducers/autoScrobbleReducer.ts +++ b/components/profile/reducers/autoScrobbleReducer.ts @@ -1,7 +1,11 @@ import { - RECEIVED_AUTO_SCROBBLES, SET_LOADING_STATE, SET_ERROR_STATE, START_DELETING, - END_DELETING, REMOVE_AUTO_SCROBBLE, -} from '../constants/autoScrobbleConstants'; + RECEIVED_AUTO_SCROBBLES, + SET_LOADING_STATE, + SET_ERROR_STATE, + START_DELETING, + END_DELETING, + REMOVE_AUTO_SCROBBLE, +} from "../constants/autoScrobbleConstants"; const initialState = { data: null, diff --git a/components/profile/reducers/historyReducer.ts b/components/profile/reducers/historyReducer.ts index b6912e8..7b54e83 100644 --- a/components/profile/reducers/historyReducer.ts +++ b/components/profile/reducers/historyReducer.ts @@ -1,4 +1,4 @@ -import { RECEIVED_HISTORY, SET_LOADING_STATE, SET_ERROR_STATE } from '../constants/historyConstants'; +import { RECEIVED_HISTORY, SET_LOADING_STATE, SET_ERROR_STATE } from "../constants/historyConstants"; const historyReducer = (state = {}, action: any = {}) => { switch (action.type) { diff --git a/components/profile/reducers/spec/autoScrobbleReducer.spec.ts b/components/profile/reducers/spec/autoScrobbleReducer.spec.ts index 1250c2f..2273ab8 100644 --- a/components/profile/reducers/spec/autoScrobbleReducer.spec.ts +++ b/components/profile/reducers/spec/autoScrobbleReducer.spec.ts @@ -1,11 +1,15 @@ -import autoScrobbleReducer from '../autoScrobbleReducer'; +import autoScrobbleReducer from "../autoScrobbleReducer"; import { - setLoadingState, setErrorState, receivedAutoScrobbles, startDeleting, removeAutoScrobble, + setLoadingState, + setErrorState, + receivedAutoScrobbles, + startDeleting, + removeAutoScrobble, endDeleting, -} from '../../actions/autoScrobbleActionCreators'; +} from "../../actions/autoScrobbleActionCreators"; -describe('autoScrobbleReducer', () => { - it('uses the correct initial state', () => { +describe("autoScrobbleReducer", () => { + it("uses the correct initial state", () => { expect(autoScrobbleReducer()).toEqual({ data: null, deleting: [], @@ -14,11 +18,11 @@ describe('autoScrobbleReducer', () => { }); }); - it('saves autoScrobbles to store', () => { + it("saves autoScrobbles to store", () => { const state = { data: null, }; - const autoScrobbles = ['foo', 'bar']; + const autoScrobbles = ["foo", "bar"]; const action = receivedAutoScrobbles(autoScrobbles); const nextState = autoScrobbleReducer(state, action); @@ -27,7 +31,7 @@ describe('autoScrobbleReducer', () => { }); }); - it('saves loading state to store', () => { + it("saves loading state to store", () => { const state = { loading: false, }; @@ -39,11 +43,11 @@ describe('autoScrobbleReducer', () => { }); }); - it('saves error state to store', () => { + it("saves error state to store", () => { const state = { error: null, }; - const error = 'FooBar'; + const error = "FooBar"; const action = setErrorState(error); const nextState = autoScrobbleReducer(state, action); @@ -52,42 +56,42 @@ describe('autoScrobbleReducer', () => { }); }); - it('saves starting of delete-process to store', () => { + it("saves starting of delete-process to store", () => { const state = { - deleting: ['foo'], + deleting: ["foo"], }; - const deleting = 'bar'; + const deleting = "bar"; const action = startDeleting(deleting); const nextState = autoScrobbleReducer(state, action); expect(nextState).toEqual({ - deleting: ['foo', 'bar'], + deleting: ["foo", "bar"], }); }); - it('removes autoScrobble from store', () => { + it("removes autoScrobble from store", () => { const state = { - data: [{ id: 'foo' }, { id: 'bar' }], + data: [{ id: "foo" }, { id: "bar" }], }; - const deleting = 'foo'; + const deleting = "foo"; const action = removeAutoScrobble(deleting); const nextState = autoScrobbleReducer(state, action); expect(nextState).toEqual({ - data: [{ id: 'bar' }], + data: [{ id: "bar" }], }); }); - it('saves ending of delete-process to store', () => { + it("saves ending of delete-process to store", () => { const state = { - deleting: ['foo', 'bar'], + deleting: ["foo", "bar"], }; - const deleting = 'bar'; + const deleting = "bar"; const action = endDeleting(deleting); const nextState = autoScrobbleReducer(state, action); expect(nextState).toEqual({ - deleting: ['foo'], + deleting: ["foo"], }); }); }); diff --git a/components/profile/reducers/spec/historyReducer.spec.ts b/components/profile/reducers/spec/historyReducer.spec.ts index 3e25f6c..9bc089a 100644 --- a/components/profile/reducers/spec/historyReducer.spec.ts +++ b/components/profile/reducers/spec/historyReducer.spec.ts @@ -1,16 +1,16 @@ -import historyReducer from '../historyReducer'; -import { setLoadingState, setErrorState, receivedHistory } from '../../actions/historyActionCreators'; +import historyReducer from "../historyReducer"; +import { setLoadingState, setErrorState, receivedHistory } from "../../actions/historyActionCreators"; -describe('historyReducer', () => { - it('uses an empty object as initial state', () => { +describe("historyReducer", () => { + it("uses an empty object as initial state", () => { expect(historyReducer()).toEqual({}); }); - it('saves history to store', () => { + it("saves history to store", () => { const state = { data: null, }; - const history = ['foo', 'bar']; + const history = ["foo", "bar"]; const action = receivedHistory(history); const nextState = historyReducer(state, action); @@ -19,7 +19,7 @@ describe('historyReducer', () => { }); }); - it('saves loading state to store', () => { + it("saves loading state to store", () => { const state = { loading: false, }; @@ -31,11 +31,11 @@ describe('historyReducer', () => { }); }); - it('saves error state to store', () => { + it("saves error state to store", () => { const state = { error: null, }; - const error = 'FooBar'; + const error = "FooBar"; const action = setErrorState(error); const nextState = historyReducer(state, action); diff --git a/components/query/QueryRelease.tsx b/components/query/QueryRelease.tsx index bf94131..2592449 100644 --- a/components/query/QueryRelease.tsx +++ b/components/query/QueryRelease.tsx @@ -1,29 +1,44 @@ -import { bindActionCreators } from 'redux'; -import { connect, ConnectedProps } from 'react-redux'; -import React from 'react'; -import Head from 'next/head'; -import Link from 'next/link'; -import { IoIosSearch } from 'react-icons/io'; -import { MdClose } from 'react-icons/md'; -import compact from 'lodash/compact'; -import { trackEvent } from '../../lib/analytics'; -import { silver } from '../../lib/colors'; -import NoResultsIcon from '../icons/NoResultsIcon'; -import Loading from '../layout/Loading'; -import { queryRelease, resetResults, setQuery } from './actions/queryActions'; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import type { ConnectedProps } from "react-redux"; +import React from "react"; +import Head from "next/head"; +import Link from "next/link"; +import { IoIosSearch } from "react-icons/io"; +import { MdClose } from "react-icons/md"; +import compact from "lodash/compact"; +import { trackEvent } from "../../lib/analytics"; +import { silver } from "../../lib/colors"; +import NoResultsIcon from "../icons/NoResultsIcon"; +import Loading from "../layout/Loading"; +import { queryRelease, resetResults, setQuery } from "./actions/queryActions"; import { - Button, CloseButton, Content, FallbackIcon, FallbackWrapper, HeadWrapper, Icon, Input, - LoadingWrapper, Meta, Overlay, Result, ResultInfo, ResultWrapper, Submit, Thumbnail, - ThumbnailWrapper, Title, Wrapper, -} from './styles/QueryRelease.styles'; + Button, + CloseButton, + Content, + FallbackIcon, + FallbackWrapper, + HeadWrapper, + Icon, + Input, + LoadingWrapper, + Meta, + Overlay, + Result, + ResultInfo, + ResultWrapper, + Submit, + Thumbnail, + ThumbnailWrapper, + Title, + Wrapper, +} from "./styles/QueryRelease.styles"; const mapStateToProps = (state: any) => ({ ...(state.query as { results?: any[]; query?: string; loading?: boolean }), }); -const mapDispatchToProps = (dispatch: any) => ( - bindActionCreators({ queryRelease, resetResults, setQuery }, dispatch) -); +const mapDispatchToProps = (dispatch: any) => bindActionCreators({ queryRelease, resetResults, setQuery }, dispatch); const connector = connect(mapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; @@ -34,48 +49,56 @@ class QueryRelease extends React.Component { this.props.resetResults(); this.props.setQuery(); this.setState({ searched: false }); - } + }; open = () => { this.reset(); - trackEvent('Detect', 'Query Release'); + trackEvent("Detect", "Query Release"); this.setState({ open: true }); - } + }; close = () => { this.setState({ open: false, searched: false }); - } + }; - onInput = (e) => { + onInput = e => { this.props.setQuery(e.target.value); - } + }; - onSubmit = (e) => { + onSubmit = e => { e.preventDefault(); this.inputRef.current.blur(); this.props.queryRelease(); this.setState({ searched: true }); - } + }; render() { const { open, searched } = this.state; - const { loading = false, results = [], query = '' } = this.props; + const { loading = false, results = [], query = "" } = this.props; - let content = ; + let content = ( + + + + ); if (loading) { - content = ; + content = ( + + + + + + ); } else if (results.length) { content = ( - {results.map(({ - id, title, thumb, country, year, format = [], - }) => ( + {results.map(({ id, title, thumb, country, year, format = [] }) => ( @@ -86,9 +109,7 @@ class QueryRelease extends React.Component - - {compact([country, (format || []).join(', ')]).join(' · ')} - + {compact([country, (format || []).join(", ")]).join(" · ")} @@ -98,7 +119,7 @@ class QueryRelease extends React.Component - + No results were found
for your query @@ -121,7 +142,15 @@ class QueryRelease extends React.Component - + {/* eslint-disable jsx-a11y/no-autofocus */} + + {/* eslint-enable jsx-a11y/no-autofocus */} diff --git a/components/query/actions/queryActionCreators.ts b/components/query/actions/queryActionCreators.ts index 669060d..cec7fb0 100644 --- a/components/query/actions/queryActionCreators.ts +++ b/components/query/actions/queryActionCreators.ts @@ -1,6 +1,4 @@ -import { - SET_LOADING_STATE, SET_ERROR_STATE, SET_QUERY_STRING, RECEIVED_RESULTS, -} from '../constants/queryConstants'; +import { SET_LOADING_STATE, SET_ERROR_STATE, SET_QUERY_STRING, RECEIVED_RESULTS } from "../constants/queryConstants"; export const setLoadingState = loading => ({ type: SET_LOADING_STATE, diff --git a/components/query/actions/queryActions.ts b/components/query/actions/queryActions.ts index 9d3ce44..a24f944 100644 --- a/components/query/actions/queryActions.ts +++ b/components/query/actions/queryActions.ts @@ -1,30 +1,26 @@ -import { - setLoadingState, setErrorState, setQueryString, receivedResults, -} from './queryActionCreators'; +import { setLoadingState, setErrorState, setQueryString, receivedResults } from "./queryActionCreators"; -export const queryRelease = () => ( - async (dispatch, getState) => { - try { - dispatch(setLoadingState(true)); - const { query } = getState().query; - const data = await fetch(`/api/search/${encodeURIComponent(query)}`, { credentials: 'include' }).then(r => r.json()); - dispatch(receivedResults(data)); - } catch (error) { - dispatch(receivedResults([])); - dispatch(setErrorState(error)); - } - dispatch(setLoadingState(false)); +export const queryRelease = () => async (dispatch, getState) => { + try { + dispatch(setLoadingState(true)); + const { query } = getState().query; + const data = await fetch(`/api/search/${encodeURIComponent(query)}`, { credentials: "include" }).then(r => + r.json(), + ); + dispatch(receivedResults(data)); + } catch (error) { + dispatch(receivedResults([])); + dispatch(setErrorState(error)); } -); + dispatch(setLoadingState(false)); +}; -export const setQuery = (query = '') => ( - (dispatch) => { +export const setQuery = + (query = "") => + dispatch => { dispatch(setQueryString(query)); - } -); + }; -export const resetResults = () => ( - (dispatch) => { - dispatch(receivedResults([])); - } -); +export const resetResults = () => dispatch => { + dispatch(receivedResults([])); +}; diff --git a/components/query/constants/queryConstants.ts b/components/query/constants/queryConstants.ts index 13729ba..b5d8c50 100644 --- a/components/query/constants/queryConstants.ts +++ b/components/query/constants/queryConstants.ts @@ -1,4 +1,4 @@ -export const SET_LOADING_STATE = 'SET_QUERY_LOADING_STATE'; -export const SET_ERROR_STATE = 'SET_QUERY_ERROR_STATE'; -export const SET_QUERY_STRING = 'SET_QUERY_STRING'; -export const RECEIVED_RESULTS = 'RECEIVED_QUERY_RESULTS'; +export const SET_LOADING_STATE = "SET_QUERY_LOADING_STATE"; +export const SET_ERROR_STATE = "SET_QUERY_ERROR_STATE"; +export const SET_QUERY_STRING = "SET_QUERY_STRING"; +export const RECEIVED_RESULTS = "RECEIVED_QUERY_RESULTS"; diff --git a/components/query/reducers/queryReducer.ts b/components/query/reducers/queryReducer.ts index 82fb66f..e5a0bfe 100644 --- a/components/query/reducers/queryReducer.ts +++ b/components/query/reducers/queryReducer.ts @@ -1,9 +1,7 @@ -import { - SET_LOADING_STATE, SET_ERROR_STATE, SET_QUERY_STRING, RECEIVED_RESULTS, -} from '../constants/queryConstants'; +import { SET_LOADING_STATE, SET_ERROR_STATE, SET_QUERY_STRING, RECEIVED_RESULTS } from "../constants/queryConstants"; const initialState = { - query: '', + query: "", results: [], error: null, loading: false, diff --git a/components/query/styles/QueryRelease.styles.ts b/components/query/styles/QueryRelease.styles.ts index ce022ab..c1eedf4 100644 --- a/components/query/styles/QueryRelease.styles.ts +++ b/components/query/styles/QueryRelease.styles.ts @@ -1,8 +1,8 @@ -import { IoIosSearch } from 'react-icons/io'; -import styled from 'styled-components'; -import LogoIcon from '../../icons/LogoIcon'; -import { dark } from '../../../lib/colors'; -import { buttonReset } from '../../../styles/mixins'; +import { IoIosSearch } from "react-icons/io"; +import styled from "styled-components"; +import LogoIcon from "../../icons/LogoIcon"; +import { dark } from "../../../lib/colors"; +import { buttonReset } from "../../../styles/mixins"; export const Wrapper = styled.div` position: relative; @@ -43,7 +43,7 @@ export const Overlay = styled.div` width: 100%; height: 100%; overflow: auto; - background: rgba(0,0,0,.7); + background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(10px); -webkit-overflow-scrolling: touch; `; diff --git a/components/release/ReleaseInfo.tsx b/components/release/ReleaseInfo.tsx index 6ffe00a..18c0549 100644 --- a/components/release/ReleaseInfo.tsx +++ b/components/release/ReleaseInfo.tsx @@ -1,13 +1,29 @@ -import React from 'react'; -import { MdClose } from 'react-icons/md'; // TODO: replace -import durationFormat from '../../lib/durationFormat'; -import targetBlank from '../../lib/targetBlank'; +import React from "react"; +import { MdClose } from "react-icons/md"; // TODO: replace +import durationFormat from "../../lib/durationFormat"; +import targetBlank from "../../lib/targetBlank"; import { - Artist, Button, CloseButton, Content, Cover, ExternalButton, Head, HeadWrapper, Icon, Meta, - Overlay, Title, TrackDuration, TrackListWrapper, TrackNumber, TrackTitle, Wrapper, Year, -} from './styles/ReleaseInfo.styles'; -import { silver } from '../../lib/colors'; -import { autotrackParams, trackEvent } from '../../lib/analytics'; + Artist, + Button, + CloseButton, + Content, + Cover, + ExternalButton, + Head, + HeadWrapper, + Icon, + Meta, + Overlay, + Title, + TrackDuration, + TrackListWrapper, + TrackNumber, + TrackTitle, + Wrapper, + Year, +} from "./styles/ReleaseInfo.styles"; +import { silver } from "../../lib/colors"; +import { autotrackParams, trackEvent } from "../../lib/analytics"; type Track = { trackNumber: string; @@ -31,21 +47,17 @@ interface ReleaseInfoProps { class ReleaseInfo extends React.Component { state = { open: false, - } + }; handleButton = () => { const { open } = this.state; - if (!open) trackEvent('Detected', 'Show Release Info'); + if (!open) trackEvent("Detected", "Show Release Info"); this.setState(state => ({ open: !state.open })); - } + }; render() { const { open } = this.state; - const { - release: { - image, title, year, artist, tracks, url, - } = {} as Release, - } = this.props; + const { release: { image, title, year, artist, tracks, url } = {} as Release } = this.props; return ( - @@ -70,11 +68,6 @@ const mapStateToProps = (state, { code }) => ({ ...state.release[code], }); -const mapDispatchToProps = dispatch => ( - bindActionCreators({ fetchRelease }, dispatch) -); +const mapDispatchToProps = dispatch => bindActionCreators({ fetchRelease }, dispatch); -export default connect( - mapStateToProps, - mapDispatchToProps, -)(SearchRelease) as any; +export default connect(mapStateToProps, mapDispatchToProps)(SearchRelease) as any; diff --git a/components/release/SearchReleaseError.tsx b/components/release/SearchReleaseError.tsx index 57e1647..a95aca7 100644 --- a/components/release/SearchReleaseError.tsx +++ b/components/release/SearchReleaseError.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { IoIosRefresh } from 'react-icons/io'; -import { yellow } from '../../lib/colors'; -import { FlexContent } from '../../styles/layout.styles'; -import { ErrorIcon, RetryButton } from '../layout/styles/Error.styles'; +import React from "react"; +import { IoIosRefresh } from "react-icons/io"; +import { yellow } from "../../lib/colors"; +import { FlexContent } from "../../styles/layout.styles"; +import { ErrorIcon, RetryButton } from "../layout/styles/Error.styles"; interface SearchReleaseErrorProps { code: string; @@ -12,7 +12,11 @@ interface SearchReleaseErrorProps { const SearchReleaseError = ({ code, onRetry }: SearchReleaseErrorProps) => ( - No release found
{code}
+ + No release found +
+ {code} +
Retry diff --git a/components/release/actions/releaseActionCreators.ts b/components/release/actions/releaseActionCreators.ts index c197b35..77836d3 100644 --- a/components/release/actions/releaseActionCreators.ts +++ b/components/release/actions/releaseActionCreators.ts @@ -1,4 +1,4 @@ -import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_RELEASE } from '../constants/releaseConstants'; +import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_RELEASE } from "../constants/releaseConstants"; export const setLoadingState = (code, loading) => ({ type: SET_LOADING_STATE, diff --git a/components/release/actions/releaseActions.ts b/components/release/actions/releaseActions.ts index 9d9a2ba..3e3beb3 100644 --- a/components/release/actions/releaseActions.ts +++ b/components/release/actions/releaseActions.ts @@ -1,33 +1,29 @@ -import { setLoadingState, receivedRelease, setErrorState } from './releaseActionCreators'; +import { setLoadingState, receivedRelease, setErrorState } from "./releaseActionCreators"; -const shouldFetchRelease = release => (!release || !release.data?.id); +const shouldFetchRelease = release => !release || !release.data?.id; -export const fetchRelease = code => ( - async (dispatch) => { - try { - dispatch(setErrorState(code, null)); - dispatch(setLoadingState(code, true)); +export const fetchRelease = code => async dispatch => { + try { + dispatch(setErrorState(code, null)); + dispatch(setLoadingState(code, true)); - const response = await fetch(`/api/barcode/${code}`, { credentials: 'include' }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const data = await response.json(); + const response = await fetch(`/api/barcode/${code}`, { credentials: "include" }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); - if (data.id) { - dispatch(receivedRelease(code, data)); - } else { - dispatch(setErrorState(code, 'No release found')); - } - } catch (error) { - dispatch(setErrorState(code, error)); + if (data.id) { + dispatch(receivedRelease(code, data)); + } else { + dispatch(setErrorState(code, "No release found")); } - dispatch(setLoadingState(code, false)); + } catch (error) { + dispatch(setErrorState(code, error)); } -); + dispatch(setLoadingState(code, false)); +}; -export const fetchReleaseIfNeeded = code => ( - (dispatch, getState) => { - if (shouldFetchRelease(getState().release[code])) { - dispatch(fetchRelease(code)); - } +export const fetchReleaseIfNeeded = code => (dispatch, getState) => { + if (shouldFetchRelease(getState().release[code])) { + dispatch(fetchRelease(code)); } -); +}; diff --git a/components/release/constants/releaseConstants.ts b/components/release/constants/releaseConstants.ts index 14307e1..38d7e4b 100644 --- a/components/release/constants/releaseConstants.ts +++ b/components/release/constants/releaseConstants.ts @@ -1,3 +1,3 @@ -export const SET_LOADING_STATE = 'SET_RELEASE_LOADING_STATE'; -export const SET_ERROR_STATE = 'SET_RELEASE_ERROR_STATE'; -export const RECEIVED_RELEASE = 'RECEIVED_RELEASE'; +export const SET_LOADING_STATE = "SET_RELEASE_LOADING_STATE"; +export const SET_ERROR_STATE = "SET_RELEASE_ERROR_STATE"; +export const RECEIVED_RELEASE = "RECEIVED_RELEASE"; diff --git a/components/release/reducers/releaseReducer.ts b/components/release/reducers/releaseReducer.ts index ab02b84..64d6651 100644 --- a/components/release/reducers/releaseReducer.ts +++ b/components/release/reducers/releaseReducer.ts @@ -1,4 +1,4 @@ -import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_RELEASE } from '../constants/releaseConstants'; +import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_RELEASE } from "../constants/releaseConstants"; const releaseReducer = (state: any = {}, action: any = {}) => { switch (action.type) { diff --git a/components/release/styles/ReleaseInfo.styles.ts b/components/release/styles/ReleaseInfo.styles.ts index c6f487e..799610f 100644 --- a/components/release/styles/ReleaseInfo.styles.ts +++ b/components/release/styles/ReleaseInfo.styles.ts @@ -1,6 +1,6 @@ -import { IoIosInformationCircleOutline } from 'react-icons/io'; -import styled from 'styled-components'; -import { dark } from '../../../lib/colors'; +import { IoIosInformationCircleOutline } from "react-icons/io"; +import styled from "styled-components"; +import { dark } from "../../../lib/colors"; export const Wrapper = styled.div` position: relative; @@ -41,7 +41,7 @@ export const Overlay = styled.div` width: 100%; height: 100%; overflow: auto; - background: rgba(0,0,0,.7); + background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(10px); -webkit-overflow-scrolling: touch; `; @@ -79,7 +79,7 @@ export const Cover = styled.img` height: 100%; object-fit: cover; filter: blur(10px); - opacity: .4; + opacity: 0.4; `; export const Meta = styled.div` @@ -97,12 +97,12 @@ export const Title = styled.div` `; export const Year = styled.div` - font-size: .5em; + font-size: 0.5em; `; export const Artist = styled.div` margin-bottom: 8px; - font-size: .8em; + font-size: 0.8em; `; export const TrackListWrapper = styled.div` diff --git a/components/release/styles/SearchRelease.styles.ts b/components/release/styles/SearchRelease.styles.ts index 638b0f4..a2157d8 100644 --- a/components/release/styles/SearchRelease.styles.ts +++ b/components/release/styles/SearchRelease.styles.ts @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled from "styled-components"; export const Poster = styled.div<{ image?: string }>` display: flex; @@ -18,7 +18,7 @@ export const PosterContent = styled.div` display: flex; width: 100%; padding: 0 20%; - background: rgba(0, 0, 0, .5); + background: rgba(0, 0, 0, 0.5); text-align: center; backdrop-filter: blur(10px); `; diff --git a/components/scanner/Scanner.tsx b/components/scanner/Scanner.tsx index ecb26ee..5cf5b85 100644 --- a/components/scanner/Scanner.tsx +++ b/components/scanner/Scanner.tsx @@ -1,10 +1,10 @@ -import React from 'react'; -import Quagga from 'quagga'; -import { yellow } from '../../lib/colors'; -import { FlexContent } from '../../styles/layout.styles'; -import { ErrorDescription, ErrorIcon } from '../layout/styles/Error.styles'; -import { Camera } from './styles/Scanner.styles'; -import Loading from '../layout/Loading'; +import React from "react"; +import Quagga from "quagga"; +import { yellow } from "../../lib/colors"; +import { FlexContent } from "../../styles/layout.styles"; +import { ErrorDescription, ErrorIcon } from "../layout/styles/Error.styles"; +import { Camera } from "./styles/Scanner.styles"; +import Loading from "../layout/Loading"; interface ScannerProps { onDetected: (result: any) => void; @@ -21,36 +21,39 @@ class Scanner extends React.Component { + if (err) { + this.setState({ videoError: true, loading: false }); + return; + } + this.onInitSuccess(); }, - }, (err) => { - if (err) { - this.setState({ videoError: true, loading: false }); - return; - } - this.onInitSuccess(); - }); + ); Quagga.onDetected(this.onDetected); } } @@ -62,33 +65,33 @@ class Scanner extends React.Component { Quagga.start(); this.setState({ loading: false }); - } + }; - onDetected = (result) => { + onDetected = result => { const { onDetected } = this.props; this.setState({ loading: true }); Quagga.offDetected(this.onDetected); onDetected(result); - } + }; render() { const { videoError, loading } = this.state; const ready = !loading && !videoError; return ( <> - {loading && } + {loading && } {videoError && ( An error occurred - - Please make sure this website is allowed to use the camera. - + Please make sure this website is allowed to use the camera. )} - {/* eslint-disable-next-line jsx-a11y/media-has-caption */} - + + {/* eslint-disable-next-line jsx-a11y/media-has-caption */} + ); } diff --git a/components/scanner/styles/Scanner.styles.ts b/components/scanner/styles/Scanner.styles.ts index d8412c3..c8765a8 100644 --- a/components/scanner/styles/Scanner.styles.ts +++ b/components/scanner/styles/Scanner.styles.ts @@ -1,14 +1,15 @@ -import styled from 'styled-components'; +import styled from "styled-components"; // eslint-disable-next-line import/prefer-default-export export const Camera = styled.div<{ $visible?: boolean }>` - visibility: ${props => (props.$visible ? 'visible' : 'hidden')}; + visibility: ${props => (props.$visible ? "visible" : "hidden")}; position: absolute; width: 100%; height: 100%; transform: translate3d(0, 0, 0); - video, canvas { + video, + canvas { position: absolute; top: 0; left: 0; diff --git a/components/scrobble/Scrobble.tsx b/components/scrobble/Scrobble.tsx index c7a1401..a118bb1 100644 --- a/components/scrobble/Scrobble.tsx +++ b/components/scrobble/Scrobble.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { Loading, LoadingContent, LoadingWrapper } from './styles/Scrobble.styles'; -import ScrobbleError from './ScrobbleError'; +import React from "react"; +import { Loading, LoadingContent, LoadingWrapper } from "./styles/Scrobble.styles"; +import ScrobbleError from "./ScrobbleError"; interface ScrobbleProps { release: { id: string; image?: string; [key: string]: any }; @@ -18,34 +18,36 @@ class Scrobble extends React.Component } doRequest = async () => { - const { release: { id }, autoScrobble, onScrobbled } = this.props; + const { + release: { id }, + autoScrobble, + onScrobbled, + } = this.props; try { this.setState({ loadingError: false }); - await fetch('/api/scrobble', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + await fetch("/api/scrobble", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, autoScrobble }), }); - } catch (error) { + } catch { return this.setState({ loadingError: true }); } return onScrobbled(); - } + }; render() { const { loadingError } = this.state; const { release } = this.props; - return ( - loadingError - ? - : ( - - - Sending data to Last.fm - - ) + return loadingError ? ( + + ) : ( + + + Sending data to Last.fm + ); } } diff --git a/components/scrobble/ScrobbleError.tsx b/components/scrobble/ScrobbleError.tsx index 6965b89..c98f73f 100644 --- a/components/scrobble/ScrobbleError.tsx +++ b/components/scrobble/ScrobbleError.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { IoIosRefresh } from 'react-icons/io'; -import { yellow } from '../../lib/colors'; -import { FlexContent } from '../../styles/layout.styles'; -import { ErrorIcon, RetryButton } from '../layout/styles/Error.styles'; +import React from "react"; +import { IoIosRefresh } from "react-icons/io"; +import { yellow } from "../../lib/colors"; +import { FlexContent } from "../../styles/layout.styles"; +import { ErrorIcon, RetryButton } from "../layout/styles/Error.styles"; interface ScrobbleErrorProps { onRetry: () => void; diff --git a/components/scrobble/styles/Scrobble.styles.ts b/components/scrobble/styles/Scrobble.styles.ts index c24e741..3447303 100644 --- a/components/scrobble/styles/Scrobble.styles.ts +++ b/components/scrobble/styles/Scrobble.styles.ts @@ -1,5 +1,5 @@ -import styled from 'styled-components'; -import Spinner from '../../layout/Spinner'; +import styled from "styled-components"; +import Spinner from "../../layout/Spinner"; export const Loading = styled(Spinner)` width: 100%; @@ -23,8 +23,11 @@ export const LoadingContent = styled.div` left: 0; width: 100%; padding: 12% 20%; - background: rgba(0, 0, 0, .5); + background: rgba(0, 0, 0, 0.5); text-align: center; backdrop-filter: blur(10px); - text-shadow: 0 0 3px black, 0 0 3px black, 0 0 3px black; + text-shadow: + 0 0 3px black, + 0 0 3px black, + 0 0 3px black; `; diff --git a/components/session/Session.tsx b/components/session/Session.tsx index 6bd3eba..c766789 100644 --- a/components/session/Session.tsx +++ b/components/session/Session.tsx @@ -1,15 +1,13 @@ -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import React from 'react'; -import Head from 'next/head'; -import Link from 'next/link'; -import Router from 'next/router'; -import { fetchSessionIfNeeded } from './actions/sessionActions'; -import { - Arrow, Image, ImageAndUser, Loader, Menu, MenuItem, Username, -} from './styles/Session.styles'; -import targetBlank from '../../lib/targetBlank'; -import { autotrackParams } from '../../lib/analytics'; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import React from "react"; +import Head from "next/head"; +import Link from "next/link"; +import Router from "next/router"; +import { fetchSessionIfNeeded } from "./actions/sessionActions"; +import { Arrow, Avatar, ImageAndUser, Loader, Menu, MenuItem, Username } from "./styles/Session.styles"; +import targetBlank from "../../lib/targetBlank"; +import { autotrackParams } from "../../lib/analytics"; interface SessionProps { session?: any; @@ -22,35 +20,35 @@ class Session extends React.Component { state = { open: false, - } + }; componentDidMount() { this.props.fetchSessionIfNeeded(); - document.addEventListener('click', this.handleClickOutside); + document.addEventListener("click", this.handleClickOutside); } componentDidUpdate(prevProps: SessionProps) { if (!prevProps.error && this.props.error) { - Router.push('/login'); + Router.push("/login"); } } componentWillUnmount() { - document.removeEventListener('click', this.handleClickOutside); + document.removeEventListener("click", this.handleClickOutside); } - handleClickOutside = (event) => { + handleClickOutside = event => { const { open } = this.state; const ref = this.overlayRef.current; if (open && !ref.contains(event.target) && document.body.contains(event.target)) { this.setState({ open: false }); } - } + }; handleClick = () => { this.setState(state => ({ open: !state.open })); - } + }; render() { const { session = {}, error = null } = this.props; @@ -65,22 +63,24 @@ class Session extends React.Component { {session && session.name ? ( <> - {session.name} - + + {session.name} + + ) : ( - + - + )} - Profile + Profile - Logout + Logout @@ -93,11 +93,6 @@ const mapStateToProps = state => ({ error: state.session.error, }); -const mapDispatchToProps = dispatch => ( - bindActionCreators({ fetchSessionIfNeeded }, dispatch) -); +const mapDispatchToProps = dispatch => bindActionCreators({ fetchSessionIfNeeded }, dispatch); -export default connect( - mapStateToProps, - mapDispatchToProps, -)(Session); +export default connect(mapStateToProps, mapDispatchToProps)(Session); diff --git a/components/session/actions/sessionActionCreators.ts b/components/session/actions/sessionActionCreators.ts index f6567c7..4abdfcb 100644 --- a/components/session/actions/sessionActionCreators.ts +++ b/components/session/actions/sessionActionCreators.ts @@ -1,4 +1,4 @@ -import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_SESSION } from '../constants/sessionConstants'; +import { SET_LOADING_STATE, SET_ERROR_STATE, RECEIVED_SESSION } from "../constants/sessionConstants"; export const setLoadingState = loading => ({ type: SET_LOADING_STATE, diff --git a/components/session/actions/sessionActions.ts b/components/session/actions/sessionActions.ts index ac51c06..7ebb4dc 100644 --- a/components/session/actions/sessionActions.ts +++ b/components/session/actions/sessionActions.ts @@ -1,30 +1,26 @@ -import { setLoadingState, receivedSession, setErrorState } from './sessionActionCreators'; +import { setLoadingState, receivedSession, setErrorState } from "./sessionActionCreators"; const shouldFetchSession = session => !session.data; -export const fetchSession = () => ( - async (dispatch) => { - try { - dispatch(setLoadingState(true)); - const response = await fetch('/api/session', { credentials: 'include' }); - if (!response.ok) { - dispatch(setErrorState(response.status)); - } else { - const data = await response.json(); - dispatch(receivedSession(data)); - } - } catch (error) { - dispatch(setErrorState(error)); +export const fetchSession = () => async dispatch => { + try { + dispatch(setLoadingState(true)); + const response = await fetch("/api/session", { credentials: "include" }); + if (!response.ok) { + dispatch(setErrorState(response.status)); + } else { + const data = await response.json(); + dispatch(receivedSession(data)); } - dispatch(setLoadingState(false)); + } catch (error) { + dispatch(setErrorState(error)); } -); + dispatch(setLoadingState(false)); +}; -export const fetchSessionIfNeeded = () => ( - (dispatch, getState) => { - const state = getState().session; - if (shouldFetchSession(state)) { - dispatch(fetchSession()); - } +export const fetchSessionIfNeeded = () => (dispatch, getState) => { + const state = getState().session; + if (shouldFetchSession(state)) { + dispatch(fetchSession()); } -); +}; diff --git a/components/session/constants/sessionConstants.ts b/components/session/constants/sessionConstants.ts index 53e27f7..00cbd39 100644 --- a/components/session/constants/sessionConstants.ts +++ b/components/session/constants/sessionConstants.ts @@ -1,3 +1,3 @@ -export const SET_LOADING_STATE = 'SET_SESSION_LOADING_STATE'; -export const SET_ERROR_STATE = 'SET_SESSION_ERROR_STATE'; -export const RECEIVED_SESSION = 'RECEIVED_SESSION'; +export const SET_LOADING_STATE = "SET_SESSION_LOADING_STATE"; +export const SET_ERROR_STATE = "SET_SESSION_ERROR_STATE"; +export const RECEIVED_SESSION = "RECEIVED_SESSION"; diff --git a/components/session/reducers/sessionReducer.ts b/components/session/reducers/sessionReducer.ts index 5eae3c8..006952b 100644 --- a/components/session/reducers/sessionReducer.ts +++ b/components/session/reducers/sessionReducer.ts @@ -1,4 +1,4 @@ -import { RECEIVED_SESSION, SET_LOADING_STATE, SET_ERROR_STATE } from '../constants/sessionConstants'; +import { RECEIVED_SESSION, SET_LOADING_STATE, SET_ERROR_STATE } from "../constants/sessionConstants"; const sessionReducer = (state = {}, action: any = {}) => { switch (action.type) { diff --git a/components/session/styles/Session.styles.ts b/components/session/styles/Session.styles.ts index 99e8fe3..c27932a 100644 --- a/components/session/styles/Session.styles.ts +++ b/components/session/styles/Session.styles.ts @@ -1,19 +1,21 @@ -import styled, { css } from 'styled-components'; -import { dark, yellow, yellowRGB } from '../../../lib/colors'; -import { animation } from '../../layout/Spinner'; -import { buttonReset } from '../../../styles/mixins'; +import styled, { css } from "styled-components"; +import { dark, yellow, yellowRGB } from "../../../lib/colors"; +import { animation } from "../../layout/Spinner"; +import { buttonReset } from "../../../styles/mixins"; const fadeInOnOpen = css<{ open?: boolean }>` - transition: opacity .3s; + transition: opacity 0.3s; opacity: 0; - pointer-events:none; - ${props => props.open && css` - opacity: 1; - pointer-events: auto; - `} + pointer-events: none; + ${props => + props.open && + css` + opacity: 1; + pointer-events: auto; + `} `; -export const Image = styled.div<{ image?: string }>` +export const Avatar = styled.div<{ image?: string }>` z-index: 1; width: 8vw; max-width: 50px; @@ -30,7 +32,7 @@ export const Loader = styled.div` width: 100%; height: 100%; animation: ${animation} 1s ease-in-out infinite; - border: 2px solid rgba(${yellowRGB}, .3); + border: 2px solid rgba(${yellowRGB}, 0.3); border-radius: 50%; border-top-color: ${yellow}; `; @@ -44,7 +46,7 @@ export const Arrow = styled.div<{ open?: boolean }>` height: 8px; &:before { - content: ''; + content: ""; position: absolute; top: 0; left: 50%; diff --git a/components/ui/BackButton.tsx b/components/ui/BackButton.tsx index 9b77546..3e6e09d 100644 --- a/components/ui/BackButton.tsx +++ b/components/ui/BackButton.tsx @@ -1,11 +1,9 @@ -import Router from 'next/router'; -import { ChevronLeft } from 'styled-icons/boxicons-regular'; -import { silver } from '../../lib/colors'; -import { Button } from './styles/BackButton.styles'; +import Router from "next/router"; +import { ChevronLeft } from "styled-icons/boxicons-regular"; +import { silver } from "../../lib/colors"; +import { Button } from "./styles/BackButton.styles"; -const getHostFromUrl = url => ( - (/\/\/([^/]+)\//i.exec(url) || [])[1] -); +const getHostFromUrl = url => (/\/\/([^/]+)\//i.exec(url) || [])[1]; const hasExternalReferrer = () => { if (!document.referrer) return true; @@ -14,7 +12,7 @@ const hasExternalReferrer = () => { const handleClick = () => { if (hasExternalReferrer()) { - Router.push('/'); + Router.push("/"); } else { Router.back(); } diff --git a/components/ui/Checkbox.tsx b/components/ui/Checkbox.tsx index d590675..1e25e86 100644 --- a/components/ui/Checkbox.tsx +++ b/components/ui/Checkbox.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React from "react"; -import { Input, Label, Wrapper } from './styles/Checkbox.styles'; +import { Input, Label, Wrapper } from "./styles/Checkbox.styles"; interface CheckboxProps { checked?: boolean; @@ -21,7 +21,7 @@ class Checkbox extends React.Component { shouldComponentUpdate(nextProps: CheckboxProps) { // eslint-disable-next-line react/destructuring-assignment - return ['checked', 'disabled'].some(prop => this.props[prop] !== nextProps[prop]); + return ["checked", "disabled"].some(prop => this.props[prop] !== nextProps[prop]); } handleCheck(event) { @@ -30,18 +30,11 @@ class Checkbox extends React.Component { } render() { - const { - name, className = null, checked = false, disabled = false, children = null, - } = this.props; + const { name, className = null, checked = false, disabled = false, children = null } = this.props; const id = `checkbox-${name}`; return ( - + { onChange={this.handleCheck} disabled={disabled} /> - diff --git a/components/ui/LegalLinks.tsx b/components/ui/LegalLinks.tsx index 58ab6fe..21fb5b2 100644 --- a/components/ui/LegalLinks.tsx +++ b/components/ui/LegalLinks.tsx @@ -1,5 +1,5 @@ -import NextLink from 'next/link'; -import { Link, Links } from './styles/LegalLinks.styles'; +import NextLink from "next/link"; +import { Link, Links } from "./styles/LegalLinks.styles"; const LegalLinks = () => ( diff --git a/components/ui/LoginButton.tsx b/components/ui/LoginButton.tsx index 4d4e56c..26e337f 100644 --- a/components/ui/LoginButton.tsx +++ b/components/ui/LoginButton.tsx @@ -1,5 +1,5 @@ -import LastfmIcon from '../icons/LastfmIcon'; -import { Caption, Wrapper } from './styles/LoginButton.styles'; +import LastfmIcon from "../icons/LastfmIcon"; +import { Caption, Wrapper } from "./styles/LoginButton.styles"; const LoginButton = props => ( diff --git a/components/ui/styles/BackButton.styles.ts b/components/ui/styles/BackButton.styles.ts index c9c7688..0dbbe45 100644 --- a/components/ui/styles/BackButton.styles.ts +++ b/components/ui/styles/BackButton.styles.ts @@ -1,6 +1,6 @@ -import styled from 'styled-components'; -import { silver } from '../../../lib/colors'; -import { buttonReset } from '../../../styles/mixins'; +import styled from "styled-components"; +import { silver } from "../../../lib/colors"; +import { buttonReset } from "../../../styles/mixins"; // eslint-disable-next-line import/prefer-default-export export const Button = styled.button` @@ -14,7 +14,7 @@ export const Button = styled.button` padding: 5px; overflow: hidden; border-bottom: 1px solid ${silver}; - background: rgba(0, 0, 0, .6); + background: rgba(0, 0, 0, 0.6); box-shadow: 0 0 3px 2px black; backdrop-filter: blur(5px); font-size: 16px; diff --git a/components/ui/styles/Checkbox.styles.ts b/components/ui/styles/Checkbox.styles.ts index 4106535..6d8db60 100644 --- a/components/ui/styles/Checkbox.styles.ts +++ b/components/ui/styles/Checkbox.styles.ts @@ -1,6 +1,6 @@ // Source: https://github.com/iceteabottle/css-checkbox -import styled, { keyframes } from 'styled-components'; -import { dark, silver, yellow } from '../../../lib/colors'; +import styled, { keyframes } from "styled-components"; +import { dark, silver, yellow } from "../../../lib/colors"; // custom checkbox/radios const inputHeight = 30; @@ -27,7 +27,7 @@ export const Label = styled.label` display: inline-flex; position: relative; align-items: center; - height: ${inputHeight + (2 * inputBorderWidth)}px; + height: ${inputHeight + 2 * inputBorderWidth}px; padding: 0 6px 0 42px; cursor: pointer; user-select: none; @@ -38,15 +38,15 @@ export const Label = styled.label` width: ${inputWidth}px; height: ${inputHeight}px; margin-top: ${-(inputHeight / 2 + inputBorderWidth)}px; - transition: .2s ease; + transition: 0.2s ease; transition-property: background-color, border-color; border: ${inputBorderWidth}px solid ${borderColor}; border-radius: 50%; - background: rgba(255, 255, 255, .1); + background: rgba(255, 255, 255, 0.1); text-align: center; } - &:after{ + &:after { top: 50%; left: 9px; width: 11px; @@ -54,7 +54,7 @@ export const Label = styled.label` margin-top: 0; transform: translateY(-5px) rotate(-45deg) scale(0); transform-origin: 50%; - transition: transform .2s ease-out; + transition: transform 0.2s ease-out; border-width: 0 0 3px 3px; border-style: solid; border-color: ${dark}; @@ -63,8 +63,8 @@ export const Label = styled.label` } &:before, - &:after{ - content: ''; + &:after { + content: ""; position: absolute; box-sizing: content-box; } @@ -91,13 +91,13 @@ export const Input = styled.input` &:checked { & + ${Label} { &:after { - content: ''; + content: ""; transform: translateY(-5px) rotate(-45deg) scale(1); - transition: transform .2s ease-out; + transition: transform 0.2s ease-out; border-color: ${dark}; } &:before { - animation: ${borderscale1} .2s ease-in; + animation: ${borderscale1} 0.2s ease-in; border-color: ${checkboxColor}; background: ${checkboxColor}; } diff --git a/components/ui/styles/LegalLinks.styles.ts b/components/ui/styles/LegalLinks.styles.ts index 0d2c55f..6bdb821 100644 --- a/components/ui/styles/LegalLinks.styles.ts +++ b/components/ui/styles/LegalLinks.styles.ts @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled from "styled-components"; export const Links = styled.div` display: flex; diff --git a/components/ui/styles/LoginButton.styles.ts b/components/ui/styles/LoginButton.styles.ts index 0e77c7b..7da10a7 100644 --- a/components/ui/styles/LoginButton.styles.ts +++ b/components/ui/styles/LoginButton.styles.ts @@ -1,12 +1,12 @@ -import styled from 'styled-components'; -import { lastFm, lastFmDark } from '../../../lib/colors'; +import styled from "styled-components"; +import { lastFm, lastFmDark } from "../../../lib/colors"; export const Wrapper = styled.a` display: inline-flex; flex-direction: column; align-items: flex-start; padding: 10px 25px; - transition: .25s; + transition: 0.25s; transition-property: box-shadow, transform; border: 1px solid ${lastFmDark}; border-radius: 5px; @@ -18,7 +18,7 @@ export const Wrapper = styled.a` &:active { transform: translateY(3px); - box-shadow: none + box-shadow: none; } `; diff --git a/config/setupTests.ts b/config/setupTests.ts index e1284da..4054b19 100644 --- a/config/setupTests.ts +++ b/config/setupTests.ts @@ -1,3 +1,3 @@ -import fetchMock from 'jest-fetch-mock'; +import fetchMock from "jest-fetch-mock"; global.fetch = fetchMock as any; diff --git a/eslint.config.js b/eslint.config.js index 295e748..a5a78c9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,6 @@ -const { FlatCompat } = require('@eslint/eslintrc'); -const babelParser = require('@babel/eslint-parser'); +const { FlatCompat } = require("@eslint/eslintrc"); +const js = require("@eslint/js"); +const prettierConfig = require("eslint-config-prettier/flat"); const compat = new FlatCompat({ baseDirectory: __dirname, @@ -7,44 +8,48 @@ const compat = new FlatCompat({ module.exports = [ { - ignores: ['.next/**', 'node_modules/**', 'public/**'], + ignores: [ + ".next/**", + "node_modules/**", + ".storybook/**", + "coverage/**", + "dist/**", + "build/**", + "out/**", + "public/static/**", + "test-results/**", + ], }, + js.configs.recommended, ...compat.extends( - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:jest/recommended', - 'airbnb', + "next/core-web-vitals", + "next/typescript", + "plugin:jsx-a11y/recommended", + "plugin:import/recommended", ), { + files: ["**/*.spec.{js,jsx,ts,tsx}", "**/*.test.{js,jsx,ts,tsx}"], languageOptions: { - parser: babelParser, - parserOptions: { - requireConfigFile: false, - babelOptions: { - presets: ['next/babel'], - }, - }, globals: { - browser: true, - es6: true, - node: true, + describe: "readonly", + it: "readonly", + test: "readonly", + expect: "readonly", + beforeAll: "readonly", + beforeEach: "readonly", + afterAll: "readonly", + afterEach: "readonly", + jest: "readonly", }, }, + }, + { rules: { - 'import/extensions': 'off', - 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], - 'import/no-unresolved': 'off', - 'jsx-a11y/anchor-is-valid': ['error', { - components: ['Link'], - specialLink: ['route'], - aspects: ['invalidHref', 'preferButton'], - }], - 'no-unused-vars': ['error', { args: 'none' }], - 'react/destructuring-assignment': 'off', - 'react/forbid-prop-types': 'off', - 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], - 'react/jsx-one-expression-per-line': 'off', - 'react/react-in-jsx-scope': 'off', + "@typescript-eslint/no-empty-object-type": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-require-imports": "off", + "import/no-unresolved": "off", }, }, + prettierConfig, ]; diff --git a/jest.config.js b/jest.config.js index af79000..2d7e281 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,21 +1,16 @@ module.exports = { - testEnvironment: 'jsdom', - testPathIgnorePatterns: [ - '/.next/', - '/node_modules/', - ], + testEnvironment: "jsdom", + testPathIgnorePatterns: ["/.next/", "/node_modules/"], transform: { - '\\.(js|jsx|ts|tsx)$': 'babel-jest', + "\\.(js|jsx|ts|tsx)$": "babel-jest", }, - transformIgnorePatterns: [ - '/node_modules/', - ], - setupFilesAfterEnv: ['/config/setupTests.ts'], + transformIgnorePatterns: ["/node_modules/"], + setupFilesAfterEnv: ["/config/setupTests.ts"], collectCoverageFrom: [ - 'app/**/*.{js,jsx,ts,tsx}', - 'components/**/*.{js,jsx,ts,tsx}', - 'lib/**/*.{js,jsx,ts,tsx}', - '!lib/colors.js', - '!lib/polyfills.js', + "app/**/*.{js,jsx,ts,tsx}", + "components/**/*.{js,jsx,ts,tsx}", + "lib/**/*.{js,jsx,ts,tsx}", + "!lib/colors.js", + "!lib/polyfills.js", ], }; diff --git a/lib/analytics.ts b/lib/analytics.ts index ac2a525..577c477 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -1,16 +1,17 @@ -export const ANALYTICS_ID = 'UA-135908212-1'; +export const ANALYTICS_ID = "UA-135908212-1"; export const trackEvent = (action: string, category: string, label?: string, value?: string) => { - (window as any).ga('send', 'event', category, action, label, value); + (window as any).ga("send", "event", category, action, label, value); }; export const autotrackParams = (category: string, action: string, label?: string, value?: string) => { - if ((typeof category === 'undefined' || category === null) || (typeof action === 'undefined' || action === null)) return {}; + if (typeof category === "undefined" || category === null || typeof action === "undefined" || action === null) + return {}; return { - 'data-event-category': category, - 'data-event-action': action, - 'data-event-label': label, - 'data-event-value': value, - 'data-on': 'click,auxclick,contextmenu', + "data-event-category": category, + "data-event-action": action, + "data-event-label": label, + "data-event-value": value, + "data-on": "click,auxclick,contextmenu", }; }; diff --git a/lib/colors.ts b/lib/colors.ts index a156ea3..66e8055 100644 --- a/lib/colors.ts +++ b/lib/colors.ts @@ -1,8 +1,8 @@ -export const yellow = '#feda6a'; -export const silver = '#d4d4dc'; -export const grey = '#393f4d'; -export const dark = '#1d1e22'; -export const lastFm = '#d51007'; -export const lastFmDark = '#d51007'; +export const yellow = "#feda6a"; +export const silver = "#d4d4dc"; +export const grey = "#393f4d"; +export const dark = "#1d1e22"; +export const lastFm = "#d51007"; +export const lastFmDark = "#d51007"; -export const yellowRGB = '254, 218, 106'; +export const yellowRGB = "254, 218, 106"; diff --git a/lib/durationFormat.ts b/lib/durationFormat.ts index d49252e..b40fa2f 100644 --- a/lib/durationFormat.ts +++ b/lib/durationFormat.ts @@ -1,13 +1,13 @@ export default function durationFormat(duration) { - if (duration <= 0) return ''; + if (duration <= 0) return ""; const hrs = Math.floor(duration / 3600); const mins = Math.floor((duration % 3600) / 60); const secs = Math.floor(duration % 60); - let ret = ''; - if (hrs > 0) ret += `${hrs}:${mins < 10 ? '0' : ''}`; - ret += `${mins}:${secs < 10 ? '0' : ''}`; + let ret = ""; + if (hrs > 0) ret += `${hrs}:${mins < 10 ? "0" : ""}`; + ret += `${mins}:${secs < 10 ? "0" : ""}`; ret += `${secs}`; return ret; } diff --git a/lib/initNProgress.ts b/lib/initNProgress.ts index bead7be..f27193a 100644 --- a/lib/initNProgress.ts +++ b/lib/initNProgress.ts @@ -1,5 +1,5 @@ -import Router from 'next/router'; -import NProgress from 'nprogress'; +import Router from "next/router"; +import NProgress from "nprogress"; let progressTimeout = null; @@ -11,10 +11,10 @@ const stopProgress = () => { export default () => { NProgress.configure({ showSpinner: false }); - Router.events.on('routeChangeStart', () => { + Router.events.on("routeChangeStart", () => { progressTimeout = setTimeout(NProgress.start, 100); }); - Router.events.on('routeChangeComplete', stopProgress); - Router.events.on('routeChangeError', stopProgress); + Router.events.on("routeChangeComplete", stopProgress); + Router.events.on("routeChangeError", stopProgress); }; diff --git a/lib/mongodb.ts b/lib/mongodb.ts index d8139cb..aefd61e 100644 --- a/lib/mongodb.ts +++ b/lib/mongodb.ts @@ -1,4 +1,4 @@ -import mongoose from 'mongoose'; +import mongoose from "mongoose"; let isConnected = false; diff --git a/lib/offline.ts b/lib/offline.ts index 6f90c33..88f8a48 100644 --- a/lib/offline.ts +++ b/lib/offline.ts @@ -1,8 +1,12 @@ /* eslint-disable no-console */ -if (typeof window !== 'undefined' && 'serviceWorker' in navigator) { +if (typeof window !== "undefined" && "serviceWorker" in navigator) { navigator.serviceWorker - .register('/service-worker.js') - .then(() => { console.log('Service worker registered'); }) - .catch((e) => { console.error('Error during worker registration:', e); }); + .register("/service-worker.js") + .then(() => { + console.log("Service worker registered"); + }) + .catch(e => { + console.error("Error during worker registration:", e); + }); } diff --git a/lib/session.ts b/lib/session.ts index 5080ea4..a7687ee 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -1,5 +1,5 @@ -import { getIronSession, IronSession, SessionOptions } from 'iron-session'; -import type { IncomingMessage, ServerResponse } from 'http'; +import { getIronSession, IronSession, SessionOptions } from "iron-session"; +import type { IncomingMessage, ServerResponse } from "http"; export interface SessionData { userId?: string; @@ -15,9 +15,9 @@ export interface SessionData { export const sessionOptions: SessionOptions = { password: process.env.SESSION_SECRET as string, - cookieName: 'code-scrobble-session', + cookieName: "code-scrobble-session", cookieOptions: { - secure: process.env.NODE_ENV === 'production', + secure: process.env.NODE_ENV === "production", maxAge: 2592000, // 30 days in seconds }, }; diff --git a/lib/targetBlank.ts b/lib/targetBlank.ts index 15447e1..5b3e9fe 100644 --- a/lib/targetBlank.ts +++ b/lib/targetBlank.ts @@ -1,4 +1,4 @@ export default { - target: '_blank', - rel: 'noopener noreferrer', + target: "_blank", + rel: "noopener noreferrer", }; diff --git a/lib/withAuth.ts b/lib/withAuth.ts index f490666..925abea 100644 --- a/lib/withAuth.ts +++ b/lib/withAuth.ts @@ -1,20 +1,20 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import { connectToDatabase } from './mongodb'; -import { getSession } from './session'; -import User from '../app/models/user'; +import type { NextApiRequest, NextApiResponse } from "next"; +import { connectToDatabase } from "./mongodb"; +import { getSession } from "./session"; +import User from "../app/models/user"; export async function requireUser(req: NextApiRequest, res: NextApiResponse): Promise { await connectToDatabase(); const session = await getSession(req, res); if (!session.userId) { - res.status(401).json({ error: 'Unauthorized' }); + res.status(401).json({ error: "Unauthorized" }); return null; } const user = await User.findById(session.userId); if (!user) { - res.status(401).json({ error: 'Unauthorized' }); + res.status(401).json({ error: "Unauthorized" }); return null; } diff --git a/middleware.ts b/middleware.ts index a261760..87a0136 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,17 +1,17 @@ -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; -const PUBLIC_PATHS = ['/login', '/legal', '/privacy', '/api/auth/']; +const PUBLIC_PATHS = ["/login", "/legal", "/privacy", "/api/auth/"]; export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // Skip API routes, Next.js internals, and static assets if ( - pathname.startsWith('/api/') - || pathname.startsWith('/_next/') - || pathname.startsWith('/static/') - || pathname.includes('.') + pathname.startsWith("/api/") || + pathname.startsWith("/_next/") || + pathname.startsWith("/static/") || + pathname.includes(".") ) { return NextResponse.next(); } @@ -22,14 +22,14 @@ export function middleware(request: NextRequest) { } // Redirect to login if session cookie is absent - const sessionCookie = request.cookies.get('code-scrobble-session'); + const sessionCookie = request.cookies.get("code-scrobble-session"); if (!sessionCookie) { - return NextResponse.redirect(new URL('/login', request.url)); + return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next(); } export const config = { - matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], + matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], }; diff --git a/next.config.js b/next.config.js index 39d91cc..d240877 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,5 @@ -const withBundleAnalyzer = require('@next/bundle-analyzer')({ - enabled: process.env.BUNDLE_ANALYZE === 'true', +const withBundleAnalyzer = require("@next/bundle-analyzer")({ + enabled: process.env.BUNDLE_ANALYZE === "true", }); /** @type {import('next').NextConfig} */ diff --git a/package.json b/package.json index 3f11c7e..daeb04b 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,12 @@ "test": "jest", "test:watch": "yarn test --watch", "test:coverage": "yarn test --coverage", - "lint": "eslint './**/*.{js,jsx,ts,tsx}'", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "lint:css": "stylelint './**/*.{js,jsx,ts,tsx}'", + "format": "prettier . --write", + "format:check": "prettier . --check", + "prepare": "husky", "analyze": "BUNDLE_ANALYZE=true yarn build" }, "dependencies": { @@ -49,7 +53,6 @@ }, "devDependencies": { "@babel/core": "^7.26.0", - "@babel/eslint-parser": "^7.25.9", "@babel/runtime-corejs2": "^7.26.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", @@ -63,16 +66,17 @@ "@types/react-dom": "^18.3.5", "babel-jest": "^29.7.0", "babel-plugin-styled-components": "^2.1.4", - "eslint": "^9.18.0", - "eslint-config-airbnb": "^19.0.4", + "eslint": "^8.57.0", + "eslint-config-next": "^14.2.0", + "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^28.11.0", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", + "husky": "^9.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-fetch-mock": "^3.0.1", + "lint-staged": "^16.2.4", + "prettier": "^3.8.1", "react-test-renderer": "^18.0.0", "redis-mock": "^0.47.0", "redux-mock-store": "^1.5.4", @@ -88,5 +92,14 @@ "jest-environment-jsdom>jest-util>jest-util>fsevents": false, "@babel/runtime-corejs2>core-js": false } + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "prettier --write", + "eslint --fix" + ], + "*.{json,md,yaml,yml,css,scss}": [ + "prettier --write" + ] } } diff --git a/pages/_app.tsx b/pages/_app.tsx index 7c9b398..9d4209a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,10 +1,10 @@ -import React from 'react'; -import Head from 'next/head'; -import { Provider } from 'react-redux'; -import BaseStyles from '../components/layout/BaseStyles'; -import NProgressStyles from '../styles/nprogress.styles'; -import initNProgress from '../lib/initNProgress'; -import { wrapper } from '../client/reduxStore'; +import React from "react"; +import Head from "next/head"; +import { Provider } from "react-redux"; +import BaseStyles from "../components/layout/BaseStyles"; +import NProgressStyles from "../styles/nprogress.styles"; +import initNProgress from "../lib/initNProgress"; +import { wrapper } from "../client/reduxStore"; initNProgress(); diff --git a/pages/_document.tsx b/pages/_document.tsx index 46e0cc4..6b2bef4 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,10 +1,8 @@ -import Document, { Html, Head, Main, NextScript } from 'next/document'; -import { ServerStyleSheet } from 'styled-components'; -import { ANALYTICS_ID } from '../lib/analytics'; +import Document, { Html, Head, Main, NextScript } from "next/document"; +import { ServerStyleSheet } from "styled-components"; +import { ANALYTICS_ID } from "../lib/analytics"; -const isSafari = userAgent => ( - /Version\/([0-9._]+).*Safari/.test(userAgent) -); +const isSafari = userAgent => /Version\/([0-9._]+).*Safari/.test(userAgent); export default class MyDocument extends Document { static async getInitialProps(ctx) { @@ -12,15 +10,21 @@ export default class MyDocument extends Document { const originalRenderPage = ctx.renderPage; try { - ctx.renderPage = () => originalRenderPage({ - enhanceApp: App => props => sheet.collectStyles(), - }); + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: App => props => sheet.collectStyles(), + }); const initialProps = await Document.getInitialProps(ctx); return { ...initialProps, - showManifest: ctx.req ? !isSafari(ctx.req.headers['user-agent']) : true, - styles: <>{initialProps.styles}{sheet.getStyleElement()}, + showManifest: ctx.req ? !isSafari(ctx.req.headers["user-agent"]) : true, + styles: ( + <> + {initialProps.styles} + {sheet.getStyleElement()} + + ), }; } finally { sheet.seal(); @@ -34,9 +38,10 @@ export default class MyDocument extends Document { {/* eslint-disable-next-line react/no-danger */} -