diff --git a/.lintstagedrc b/.lintstagedrc index fa394c8d..4275afd0 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,5 +1,5 @@ { - "*.{js,ts,html}": [ + "*.{ts,html}": [ "eslint --cache" ] } diff --git a/angular.json b/angular.json index 8544ffdb..ddae0df8 100644 --- a/angular.json +++ b/angular.json @@ -1112,31 +1112,6 @@ } } }, - "web-browse-feature": { - "projectType": "library", - "root": "libs/web/browse/feature", - "sourceRoot": "libs/web/browse/feature/src", - "prefix": "as", - "architect": { - "lint": { - "builder": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": [ - "libs/web/browse/feature/src/**/*.ts", - "libs/web/browse/feature/src/**/*.html" - ] - } - }, - "test": { - "builder": "@nrwl/jest:jest", - "outputs": ["coverage/libs/web/browse/feature"], - "options": { - "jestConfig": "libs/web/browse/feature/jest.config.js", - "passWithNoTests": true - } - } - } - }, "web-shell-ui-user-dropdown": { "projectType": "library", "root": "libs/web/shell/ui/user-dropdown", @@ -1257,6 +1232,177 @@ } } } + }, + "web-browse-feature-categories": { + "projectType": "library", + "root": "libs/web/browse/feature/categories", + "sourceRoot": "libs/web/browse/feature/categories/src", + "prefix": "as", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/web/browse/feature/categories/src/**/*.ts", + "libs/web/browse/feature/categories/src/**/*.html" + ] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/web/browse/feature/categories"], + "options": { + "jestConfig": "libs/web/browse/feature/categories/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "web-browse-feature-category": { + "projectType": "library", + "root": "libs/web/browse/feature/category", + "sourceRoot": "libs/web/browse/feature/category/src", + "prefix": "as", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/web/browse/feature/category/src/**/*.ts", + "libs/web/browse/feature/category/src/**/*.html" + ] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/web/browse/feature/category"], + "options": { + "jestConfig": "libs/web/browse/feature/category/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "web-browse-feature-shell": { + "projectType": "library", + "root": "libs/web/browse/feature/shell", + "sourceRoot": "libs/web/browse/feature/shell/src", + "prefix": "as", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/web/browse/feature/shell/src/**/*.ts", + "libs/web/browse/feature/shell/src/**/*.html" + ] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/web/browse/feature/shell"], + "options": { + "jestConfig": "libs/web/browse/feature/shell/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "web-browse-data-access": { + "root": "libs/web/browse/data-access", + "sourceRoot": "libs/web/browse/data-access/src", + "projectType": "library", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["libs/web/browse/data-access/**/*.ts"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/web/browse/data-access"], + "options": { + "jestConfig": "libs/web/browse/data-access/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "web-browse-ui-category-cover": { + "projectType": "library", + "root": "libs/web/browse/ui/category-cover", + "sourceRoot": "libs/web/browse/ui/category-cover/src", + "prefix": "as", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/web/browse/ui/category-cover/src/**/*.ts", + "libs/web/browse/ui/category-cover/src/**/*.html" + ] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/web/browse/ui/category-cover"], + "options": { + "jestConfig": "libs/web/browse/ui/category-cover/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "web-shared-ui-spinner": { + "projectType": "library", + "root": "libs/web/shared/ui/spinner", + "sourceRoot": "libs/web/shared/ui/spinner/src", + "prefix": "as", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/web/shared/ui/spinner/src/**/*.ts", + "libs/web/shared/ui/spinner/src/**/*.html" + ] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/web/shared/ui/spinner"], + "options": { + "jestConfig": "libs/web/shared/ui/spinner/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "web-shared-ui-playlist-list": { + "projectType": "library", + "root": "libs/web/shared/ui/playlist-list", + "sourceRoot": "libs/web/shared/ui/playlist-list/src", + "prefix": "as", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/web/shared/ui/playlist-list/src/**/*.ts", + "libs/web/shared/ui/playlist-list/src/**/*.html" + ] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/web/shared/ui/playlist-list"], + "options": { + "jestConfig": "libs/web/shared/ui/playlist-list/jest.config.js", + "passWithNoTests": true + } + } + } } } } diff --git a/apps/angular-spotify/src/custom/tailwind/_components.scss b/apps/angular-spotify/src/custom/tailwind/_components.scss index 4c6988dd..a339ea35 100644 --- a/apps/angular-spotify/src/custom/tailwind/_components.scss +++ b/apps/angular-spotify/src/custom/tailwind/_components.scss @@ -3,6 +3,10 @@ @apply px-8 pt-4; } + .text-heading { + @apply text-2xl text-white; + } + .text-description { display: -webkit-box; -webkit-line-clamp: 2; diff --git a/jest.config.js b/jest.config.js index 5e18fcaf..159f339f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -40,6 +40,12 @@ module.exports = { '/libs/web/browse/feature', '/libs/web/shell/ui/user-dropdown', '/libs/web/shell/ui/social-share', - '/libs/web/shell/ui/album-art-overlay' + '/libs/web/shell/ui/album-art-overlay', + '/libs/web/browse/feature/detail', + '/libs/web/browse/feature/shell', + '/libs/web/browse/data-access', + '/libs/web/browse/ui/category-cover', + '/libs/web/shared/ui/spinner', + '/libs/web/shared/ui/playlist-list' ] }; diff --git a/libs/web/browse/data-access/.babelrc b/libs/web/browse/data-access/.babelrc new file mode 100644 index 00000000..0cae4a9a --- /dev/null +++ b/libs/web/browse/data-access/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@nrwl/web/babel"] +} diff --git a/libs/web/browse/data-access/.eslintrc.json b/libs/web/browse/data-access/.eslintrc.json new file mode 100644 index 00000000..b710cf3a --- /dev/null +++ b/libs/web/browse/data-access/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "parserOptions": { + "project": ["libs/web/browse/data-access/tsconfig.*?.json"] + }, + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/web/browse/data-access/README.md b/libs/web/browse/data-access/README.md new file mode 100644 index 00000000..c52fca49 --- /dev/null +++ b/libs/web/browse/data-access/README.md @@ -0,0 +1,7 @@ +# web-browse-data-access + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test web-browse-data-access` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/web/browse/data-access/jest.config.js b/libs/web/browse/data-access/jest.config.js new file mode 100644 index 00000000..43fdae59 --- /dev/null +++ b/libs/web/browse/data-access/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'web-browse-data-access', + preset: '../../../../jest.preset.js', + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json' + } + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../../coverage/libs/web/browse/data-access' +}; diff --git a/libs/web/browse/data-access/src/index.ts b/libs/web/browse/data-access/src/index.ts new file mode 100644 index 00000000..9f445f8e --- /dev/null +++ b/libs/web/browse/data-access/src/index.ts @@ -0,0 +1 @@ +export * from './lib/store'; diff --git a/libs/web/browse/data-access/src/lib/store/categories/categories.action.ts b/libs/web/browse/data-access/src/lib/store/categories/categories.action.ts new file mode 100644 index 00000000..bcd07a5b --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/categories/categories.action.ts @@ -0,0 +1,22 @@ +import { GenericStoreStatus } from '@angular-spotify/web/shared/data-access/models'; +import { createAction, props } from '@ngrx/store'; + +export const loadCategories = createAction( + '[Browse Page]/Load Categories', + props>() +); + +export const loadCategoriesSuccess = createAction( + '[Browse Page/Load Categories Success', + props<{ + categories: SpotifyApi.PagingObject; + }>() +); + +export const setCategoriesState = createAction( + '[Browse Page/Set Categories state status', + props<{ + status: GenericStoreStatus; + }>() +); +// TODO: Skip load error action, to integrate with toApiResponse operator diff --git a/libs/web/browse/data-access/src/lib/store/categories/categories.effect.ts b/libs/web/browse/data-access/src/lib/store/categories/categories.effect.ts new file mode 100644 index 00000000..6bd9151c --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/categories/categories.effect.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { loadCategories, loadCategoriesSuccess, setCategoriesState } from './categories.action'; +import { switchMap, map, withLatestFrom, filter, tap } from 'rxjs/operators'; +import { BrowseApiService } from '@angular-spotify/web/shared/data-access/spotify-api'; +import { AuthStore } from '@angular-spotify/web/auth/data-access'; +import { getCategories } from './categories.selector'; +import { select, Store } from '@ngrx/store'; + +@Injectable() +export class CategoriesEffect { + loadCategories$ = createEffect(() => + this.actions$.pipe( + ofType(loadCategories), + withLatestFrom(this.store.pipe(select(getCategories))), + tap(([, categories]) => { + if (categories) { + this.store.dispatch(setCategoriesState({ status: 'success' })); + } + }), + filter(([, data]) => !data), + withLatestFrom(this.authStore.country$), + switchMap(([, country]) => + this.browseApi + .getAllCategories({ + country, + limit: 50 + }) + .pipe( + map((response) => + loadCategoriesSuccess({ + categories: response + }) + ) + ) + ) + ) + ); + + constructor( + private store: Store, + private actions$: Actions, + private browseApi: BrowseApiService, + private authStore: AuthStore + ) {} +} diff --git a/libs/web/browse/data-access/src/lib/store/categories/categories.reducer.ts b/libs/web/browse/data-access/src/lib/store/categories/categories.reducer.ts new file mode 100644 index 00000000..edd60efd --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/categories/categories.reducer.ts @@ -0,0 +1,34 @@ +import { GenericState } from '@angular-spotify/web/shared/data-access/models'; +import { createReducer, on } from '@ngrx/store'; +import { loadCategories, loadCategoriesSuccess, setCategoriesState } from './categories.action'; +export const categoriesFeatureKey = 'categories'; + +export interface CategoriesState + extends GenericState> { + map: Map; +} + +const initialState: CategoriesState = { + data: null, + status: 'pending', + error: null, + map: new Map() +}; + +export const categoriesReducer = createReducer( + initialState, + on(loadCategories, (state) => ({ ...state, status: 'loading' })), + on(loadCategoriesSuccess, (state, { categories }) => { + const { map } = state; + categories.items.forEach((category) => { + map.set(category.id, category); + }); + return { + ...state, + status: 'success', + data: categories, + map: new Map(map) + }; + }), + on(setCategoriesState, (state, { status }) => ({ ...state, status })) +); diff --git a/libs/web/browse/data-access/src/lib/store/categories/categories.selector.ts b/libs/web/browse/data-access/src/lib/store/categories/categories.selector.ts new file mode 100644 index 00000000..512008e3 --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/categories/categories.selector.ts @@ -0,0 +1,10 @@ +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import { categoriesFeatureKey, CategoriesState } from './categories.reducer'; +import { SelectorUtil } from '@angular-spotify/web/shared/utils'; + +export const getCategoriesState = createFeatureSelector(categoriesFeatureKey); +export const getCategories = createSelector(getCategoriesState, (s) => s.data); +export const getCategoriesMap = createSelector(getCategoriesState, (s) => s.map); +export const getCategoriesLoading = createSelector(getCategoriesState, SelectorUtil.isLoading); +export const getCategoryById = (categoryId: string) => + createSelector(getCategoriesMap, (map) => map.get(categoryId)); diff --git a/libs/web/browse/data-access/src/lib/store/categories/index.ts b/libs/web/browse/data-access/src/lib/store/categories/index.ts new file mode 100644 index 00000000..e9178d0f --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/categories/index.ts @@ -0,0 +1,4 @@ +export * from './categories.action'; +export * from './categories.reducer'; +export * from './categories.effect'; +export * from './categories.selector'; diff --git a/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.action.ts b/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.action.ts new file mode 100644 index 00000000..bcd35cc3 --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.action.ts @@ -0,0 +1,27 @@ +import { GenericStoreStatus } from '@angular-spotify/web/shared/data-access/models'; +import { createAction, props } from '@ngrx/store'; + +export const loadCategoryPlaylists = createAction( + '[Browse Page]/Load Category Playlist', + props<{ + categoryId: string; + params?: Record; + }>() +); + +export const loadCategoryPlaylistsSuccess = createAction( + '[Browse Page/Load Category Playlist Success', + props<{ + categoryId: string; + playlists: SpotifyApi.PagingObject; + }>() +); + +export const setCategoryPlaylistsState = createAction( + '[Browse Page/Set Category Playlist state status', + props<{ + status: GenericStoreStatus; + }>() +); + +// TODO: Skip load error action, to integrate with toApiResponse operator diff --git a/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.effect.ts b/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.effect.ts new file mode 100644 index 00000000..51dbc154 --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.effect.ts @@ -0,0 +1,51 @@ +import { BrowseApiService } from '@angular-spotify/web/shared/data-access/spotify-api'; +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { select, Store } from '@ngrx/store'; +import { EMPTY } from 'rxjs'; +import { catchError, filter, map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { + loadCategoryPlaylists, + loadCategoryPlaylistsSuccess, + setCategoryPlaylistsState +} from './category-playlists.action'; +import { getCategoryPlaylistsMap } from './category-playlists.selector'; + +@Injectable() +export class CategoryPlaylistsEffect { + loadCategoryPlaylists$ = createEffect(() => + this.actions.pipe( + ofType(loadCategoryPlaylists), + withLatestFrom(this.store.pipe(select(getCategoryPlaylistsMap))), + tap(([{ categoryId }, map]) => { + if (map?.has(categoryId)) { + this.store.dispatch( + setCategoryPlaylistsState({ + status: 'success' + }) + ); + } + }), + filter(([{ categoryId }, map]) => { + return !map?.has(categoryId); + }), + switchMap(([{ categoryId }]) => + this.browseApi.getCategoryPlaylists(categoryId).pipe( + map((playlists) => + loadCategoryPlaylistsSuccess({ + categoryId, + playlists + }) + ), + catchError(() => EMPTY) + ) + ) + ) + ); + + constructor( + private browseApi: BrowseApiService, + private actions: Actions, + private store: Store + ) {} +} diff --git a/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.reducer.ts b/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.reducer.ts new file mode 100644 index 00000000..b868fd96 --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.reducer.ts @@ -0,0 +1,37 @@ +import { GenericState } from '@angular-spotify/web/shared/data-access/models'; +import { createReducer, on } from '@ngrx/store'; +import { + loadCategoryPlaylists, + loadCategoryPlaylistsSuccess, + setCategoryPlaylistsState +} from './category-playlists.action'; + +export const categoryPlaylistsFeatureKey = 'categoryPlaylists'; + +export type CategoryPlaylistsState = GenericState< + Map> +>; + +const initialState: CategoryPlaylistsState = { + status: 'pending', + data: new Map(), + error: null +}; + +export const categoryPlaylistsReducer = createReducer( + initialState, + on(loadCategoryPlaylists, (state) => ({ + ...state, + status: 'loading' + })), + on(loadCategoryPlaylistsSuccess, (state, { categoryId, playlists }) => { + const { data: map } = state; + map?.set(categoryId, playlists); + return { + ...state, + data: new Map(map!), + status: 'success' + }; + }), + on(setCategoryPlaylistsState, (state, { status }) => ({ ...state, status })) +); diff --git a/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.selector.ts b/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.selector.ts new file mode 100644 index 00000000..7391ffdb --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/category-playlists/category-playlists.selector.ts @@ -0,0 +1,19 @@ +import { SelectorUtil } from '@angular-spotify/web/shared/utils'; +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import { categoryPlaylistsFeatureKey, CategoryPlaylistsState } from './category-playlists.reducer'; + +export const getCategoryPlaylistsState = createFeatureSelector( + categoryPlaylistsFeatureKey +); + +export const getCategoryPlaylistsLoading = createSelector( + getCategoryPlaylistsState, + SelectorUtil.isLoading +); + +export const getCategoryPlaylistsMap = createSelector(getCategoryPlaylistsState, (s) => s.data); +export const getCategoryPlaylistsById = (categoryId: string) => + createSelector(getCategoryPlaylistsMap, (map) => { + const playlists = map?.get(categoryId); + return SelectorUtil.getPlaylistsWithRoute(playlists); + }); diff --git a/libs/web/browse/data-access/src/lib/store/category-playlists/index.ts b/libs/web/browse/data-access/src/lib/store/category-playlists/index.ts new file mode 100644 index 00000000..21dbac3d --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/category-playlists/index.ts @@ -0,0 +1,4 @@ +export * from './category-playlists.action'; +export * from './category-playlists.reducer'; +export * from './category-playlists.effect'; +export * from './category-playlists.selector'; diff --git a/libs/web/browse/data-access/src/lib/store/index.ts b/libs/web/browse/data-access/src/lib/store/index.ts new file mode 100644 index 00000000..34587dd3 --- /dev/null +++ b/libs/web/browse/data-access/src/lib/store/index.ts @@ -0,0 +1,2 @@ +export * from './categories'; +export * from './category-playlists'; diff --git a/libs/web/browse/data-access/tsconfig.json b/libs/web/browse/data-access/tsconfig.json new file mode 100644 index 00000000..26b7b4af --- /dev/null +++ b/libs/web/browse/data-access/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/web/browse/data-access/tsconfig.lib.json b/libs/web/browse/data-access/tsconfig.lib.json new file mode 100644 index 00000000..63a271e6 --- /dev/null +++ b/libs/web/browse/data-access/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/libs/web/browse/data-access/tsconfig.spec.json b/libs/web/browse/data-access/tsconfig.spec.json new file mode 100644 index 00000000..541aa925 --- /dev/null +++ b/libs/web/browse/data-access/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js", "**/*.spec.jsx", "**/*.d.ts"] +} diff --git a/libs/web/browse/feature/categories/.eslintrc.json b/libs/web/browse/feature/categories/.eslintrc.json new file mode 100644 index 00000000..fad59c8c --- /dev/null +++ b/libs/web/browse/feature/categories/.eslintrc.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../../../.eslintrc.json", + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nrwl/nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "parserOptions": { + "project": ["libs/web/browse/feature/tsconfig.*?.json"] + }, + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "as", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "as", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nrwl/nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/web/browse/feature/README.md b/libs/web/browse/feature/categories/README.md similarity index 100% rename from libs/web/browse/feature/README.md rename to libs/web/browse/feature/categories/README.md diff --git a/libs/web/browse/feature/categories/jest.config.js b/libs/web/browse/feature/categories/jest.config.js new file mode 100644 index 00000000..c07b95ad --- /dev/null +++ b/libs/web/browse/feature/categories/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + displayName: 'web-browse-feature-categories', + preset: '../../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + astTransformers: { + before: [ + 'jest-preset-angular/build/InlineFilesTransformer', + 'jest-preset-angular/build/StripStylesTransformer' + ] + } + } + }, + coverageDirectory: '../../../../../coverage/libs/web/browse/feature/categories', + snapshotSerializers: [ + 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', + 'jest-preset-angular/build/AngularSnapshotSerializer.js', + 'jest-preset-angular/build/HTMLCommentSerializer.js' + ] +}; diff --git a/libs/web/browse/feature/categories/src/index.ts b/libs/web/browse/feature/categories/src/index.ts new file mode 100644 index 00000000..7b72ae19 --- /dev/null +++ b/libs/web/browse/feature/categories/src/index.ts @@ -0,0 +1 @@ +export * from './lib/categories.module'; diff --git a/libs/web/browse/feature/categories/src/lib/categories.component.html b/libs/web/browse/feature/categories/src/lib/categories.component.html new file mode 100644 index 00000000..05cf2332 --- /dev/null +++ b/libs/web/browse/feature/categories/src/lib/categories.component.html @@ -0,0 +1,9 @@ +
+
+ + +
+ +
\ No newline at end of file diff --git a/libs/web/browse/feature/categories/src/lib/categories.component.scss b/libs/web/browse/feature/categories/src/lib/categories.component.scss new file mode 100644 index 00000000..1f4870dc --- /dev/null +++ b/libs/web/browse/feature/categories/src/lib/categories.component.scss @@ -0,0 +1,5 @@ +.categories-container { + gap: --media-cover-gap; + grid-template-columns: --media-grid; + @apply grid; +} diff --git a/libs/web/browse/feature/categories/src/lib/categories.component.ts b/libs/web/browse/feature/categories/src/lib/categories.component.ts new file mode 100644 index 00000000..6168197f --- /dev/null +++ b/libs/web/browse/feature/categories/src/lib/categories.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { getCategories, getCategoriesLoading, loadCategories } from '@angular-spotify/web/browse/data-access'; + +@Component({ + selector: 'as-categories', + templateUrl: './categories.component.html', + styleUrls: ['./categories.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CategoriesComponent { + isLoading$ = this.store.pipe(select(getCategoriesLoading)) + categories$ = this.store.pipe(select(getCategories)); + + constructor(private store: Store) { + this.store.dispatch(loadCategories({})); + } +} diff --git a/libs/web/browse/feature/categories/src/lib/categories.module.ts b/libs/web/browse/feature/categories/src/lib/categories.module.ts new file mode 100644 index 00000000..232afee4 --- /dev/null +++ b/libs/web/browse/feature/categories/src/lib/categories.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CategoriesComponent } from './categories.component'; +import { CategoryCoverModule } from '@angular-spotify/web/browse/ui/category-cover'; +import { SpinnerModule } from '@angular-spotify/web/shared/ui/spinner'; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild([ + { + path: '', + component: CategoriesComponent + } + ]), + CategoryCoverModule, + SpinnerModule + ], + declarations: [CategoriesComponent], + exports: [CategoriesComponent] +}) +export class BrowseCategoriesModule {} diff --git a/libs/web/browse/feature/src/test-setup.ts b/libs/web/browse/feature/categories/src/test-setup.ts similarity index 100% rename from libs/web/browse/feature/src/test-setup.ts rename to libs/web/browse/feature/categories/src/test-setup.ts diff --git a/libs/web/browse/feature/tsconfig.json b/libs/web/browse/feature/categories/tsconfig.json similarity index 89% rename from libs/web/browse/feature/tsconfig.json rename to libs/web/browse/feature/categories/tsconfig.json index 1e370bcd..3e77fb66 100644 --- a/libs/web/browse/feature/tsconfig.json +++ b/libs/web/browse/feature/categories/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../tsconfig.base.json", "files": [], "include": [], "references": [ diff --git a/libs/web/browse/feature/tsconfig.lib.json b/libs/web/browse/feature/categories/tsconfig.lib.json similarity index 90% rename from libs/web/browse/feature/tsconfig.lib.json rename to libs/web/browse/feature/categories/tsconfig.lib.json index f5599023..2c746827 100644 --- a/libs/web/browse/feature/tsconfig.lib.json +++ b/libs/web/browse/feature/categories/tsconfig.lib.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../../../dist/out-tsc", + "outDir": "../../../../../dist/out-tsc", "target": "es2015", "declaration": true, "declarationMap": true, diff --git a/libs/web/browse/feature/tsconfig.spec.json b/libs/web/browse/feature/categories/tsconfig.spec.json similarity index 81% rename from libs/web/browse/feature/tsconfig.spec.json rename to libs/web/browse/feature/categories/tsconfig.spec.json index aed68bc6..3d77fc5c 100644 --- a/libs/web/browse/feature/tsconfig.spec.json +++ b/libs/web/browse/feature/categories/tsconfig.spec.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../../../dist/out-tsc", + "outDir": "../../../../../dist/out-tsc", "module": "commonjs", "types": ["jest", "node"] }, diff --git a/libs/web/browse/feature/category/.eslintrc.json b/libs/web/browse/feature/category/.eslintrc.json new file mode 100644 index 00000000..92f1ba7e --- /dev/null +++ b/libs/web/browse/feature/category/.eslintrc.json @@ -0,0 +1,41 @@ +{ + "extends": "../../../../../.eslintrc.json", + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nrwl/nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "parserOptions": { + "project": ["libs/web/browse/feature/detail/tsconfig.*?.json"] + }, + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "as", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "as", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nrwl/nx/angular-template"], + "rules": { + "@angular-eslint/template/no-negated-async": "off" + } + } + ] +} diff --git a/libs/web/browse/feature/category/README.md b/libs/web/browse/feature/category/README.md new file mode 100644 index 00000000..bf13efc7 --- /dev/null +++ b/libs/web/browse/feature/category/README.md @@ -0,0 +1,7 @@ +# web-browse-feature-detail + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test web-browse-feature-detail` to execute the unit tests. diff --git a/libs/web/browse/feature/category/jest.config.js b/libs/web/browse/feature/category/jest.config.js new file mode 100644 index 00000000..2f13102b --- /dev/null +++ b/libs/web/browse/feature/category/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + displayName: 'web-browse-feature-category', + preset: '../../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + astTransformers: { + before: [ + 'jest-preset-angular/build/InlineFilesTransformer', + 'jest-preset-angular/build/StripStylesTransformer' + ] + } + } + }, + coverageDirectory: '../../../../../coverage/libs/web/browse/feature/category', + snapshotSerializers: [ + 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', + 'jest-preset-angular/build/AngularSnapshotSerializer.js', + 'jest-preset-angular/build/HTMLCommentSerializer.js' + ] +}; diff --git a/libs/web/browse/feature/category/src/index.ts b/libs/web/browse/feature/category/src/index.ts new file mode 100644 index 00000000..f0e6dd7d --- /dev/null +++ b/libs/web/browse/feature/category/src/index.ts @@ -0,0 +1 @@ +export * from './lib/category.module'; diff --git a/libs/web/browse/feature/category/src/lib/category.component.html b/libs/web/browse/feature/category/src/lib/category.component.html new file mode 100644 index 00000000..41c66f31 --- /dev/null +++ b/libs/web/browse/feature/category/src/lib/category.component.html @@ -0,0 +1,8 @@ +
+

{{ category.name }} +

+ + +
\ No newline at end of file diff --git a/libs/web/browse/feature/src/lib/browse.component.scss b/libs/web/browse/feature/category/src/lib/category.component.scss similarity index 100% rename from libs/web/browse/feature/src/lib/browse.component.scss rename to libs/web/browse/feature/category/src/lib/category.component.scss diff --git a/libs/web/browse/feature/category/src/lib/category.component.ts b/libs/web/browse/feature/category/src/lib/category.component.ts new file mode 100644 index 00000000..1323fdeb --- /dev/null +++ b/libs/web/browse/feature/category/src/lib/category.component.ts @@ -0,0 +1,41 @@ +import { + getCategoryById, + getCategoryPlaylistsById, + getCategoryPlaylistsLoading, + loadCategoryPlaylists +} from '@angular-spotify/web/browse/data-access'; +import { RouterUtil } from '@angular-spotify/web/shared/utils'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { select, Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { filter, map, switchMap, tap } from 'rxjs/operators'; + +@Component({ + selector: 'as-category-detail', + templateUrl: './category.component.html', + styleUrls: ['./category.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CategoryComponent { + categoryParams$: Observable = this.route.params.pipe( + map((params) => params[RouterUtil.Configuration.CategoryId]), + filter((categoryId) => !!categoryId) + ); + + category$ = this.categoryParams$.pipe( + switchMap((categoryId) => this.store.pipe(select(getCategoryById(categoryId)))) + ); + + // TODO: find out why it is always false + isLoadingPlaylists$ = this.store.pipe(select(getCategoryPlaylistsLoading)); + + playlists$ = this.categoryParams$.pipe( + tap((categoryId) => { + this.store.dispatch(loadCategoryPlaylists({ categoryId })); + }), + switchMap((categoryId) => this.store.pipe(select(getCategoryPlaylistsById(categoryId)))) + ); + + constructor(private route: ActivatedRoute, private store: Store) {} +} diff --git a/libs/web/browse/feature/category/src/lib/category.module.ts b/libs/web/browse/feature/category/src/lib/category.module.ts new file mode 100644 index 00000000..cfbf98d5 --- /dev/null +++ b/libs/web/browse/feature/category/src/lib/category.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CategoryComponent } from './category.component'; +import { RouterModule } from '@angular/router'; +import { SpinnerModule } from '@angular-spotify/web/shared/ui/spinner'; +import { PlaylistListModule } from '@angular-spotify/web/shared/ui/playlist-list'; +@NgModule({ + imports: [ + CommonModule, + SpinnerModule, + PlaylistListModule, + RouterModule.forChild([ + { + path: '', + component: CategoryComponent + } + ]) + ], + declarations: [CategoryComponent], + exports: [CategoryComponent] +}) +export class BrowseCategoryModule {} diff --git a/libs/web/browse/feature/category/src/test-setup.ts b/libs/web/browse/feature/category/src/test-setup.ts new file mode 100644 index 00000000..8d88704e --- /dev/null +++ b/libs/web/browse/feature/category/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular'; diff --git a/libs/web/browse/feature/category/tsconfig.json b/libs/web/browse/feature/category/tsconfig.json new file mode 100644 index 00000000..3e77fb66 --- /dev/null +++ b/libs/web/browse/feature/category/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictTemplates": true + } +} diff --git a/libs/web/browse/feature/category/tsconfig.lib.json b/libs/web/browse/feature/category/tsconfig.lib.json new file mode 100644 index 00000000..2c746827 --- /dev/null +++ b/libs/web/browse/feature/category/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/web/browse/feature/category/tsconfig.spec.json b/libs/web/browse/feature/category/tsconfig.spec.json new file mode 100644 index 00000000..3d77fc5c --- /dev/null +++ b/libs/web/browse/feature/category/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/web/browse/feature/shell/.eslintrc.json b/libs/web/browse/feature/shell/.eslintrc.json new file mode 100644 index 00000000..96295b86 --- /dev/null +++ b/libs/web/browse/feature/shell/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["../../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nrwl/nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "parserOptions": { "project": ["libs/web/browse/feature/shell/tsconfig.*?.json"] }, + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { "type": "attribute", "prefix": "as", "style": "camelCase" } + ], + "@angular-eslint/component-selector": [ + "error", + { "type": "element", "prefix": "as", "style": "kebab-case" } + ] + } + }, + { "files": ["*.html"], "extends": ["plugin:@nrwl/nx/angular-template"], "rules": {} } + ] +} diff --git a/libs/web/browse/feature/shell/README.md b/libs/web/browse/feature/shell/README.md new file mode 100644 index 00000000..a2fac9e2 --- /dev/null +++ b/libs/web/browse/feature/shell/README.md @@ -0,0 +1,7 @@ +# web-browse-feature-shell + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test web-browse-feature-shell` to execute the unit tests. diff --git a/libs/web/browse/feature/shell/jest.config.js b/libs/web/browse/feature/shell/jest.config.js new file mode 100644 index 00000000..41e91200 --- /dev/null +++ b/libs/web/browse/feature/shell/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + displayName: 'web-browse-feature-shell', + preset: '../../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + astTransformers: { + before: [ + 'jest-preset-angular/build/InlineFilesTransformer', + 'jest-preset-angular/build/StripStylesTransformer' + ] + } + } + }, + coverageDirectory: '../../../../../coverage/libs/web/browse/feature/shell', + snapshotSerializers: [ + 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', + 'jest-preset-angular/build/AngularSnapshotSerializer.js', + 'jest-preset-angular/build/HTMLCommentSerializer.js' + ] +}; diff --git a/libs/web/browse/feature/shell/src/index.ts b/libs/web/browse/feature/shell/src/index.ts new file mode 100644 index 00000000..e5d5aace --- /dev/null +++ b/libs/web/browse/feature/shell/src/index.ts @@ -0,0 +1 @@ +export * from './lib/browse-shell.module'; diff --git a/libs/web/browse/feature/shell/src/lib/browse-shell.module.ts b/libs/web/browse/feature/shell/src/lib/browse-shell.module.ts new file mode 100644 index 00000000..fda6bb00 --- /dev/null +++ b/libs/web/browse/feature/shell/src/lib/browse-shell.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { RouterUtil } from '@angular-spotify/web/shared/utils'; +import { StoreModule } from '@ngrx/store'; +import { + CategoriesEffect, + categoriesFeatureKey, + categoriesReducer, + CategoryPlaylistsEffect, + categoryPlaylistsFeatureKey, + categoryPlaylistsReducer +} from '@angular-spotify/web/browse/data-access'; +import { EffectsModule } from '@ngrx/effects'; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild([ + { + path: '', + loadChildren: async () => + (await import('@angular-spotify/web/browse/feature/categories')).BrowseCategoriesModule + }, + { + path: `:${RouterUtil.Configuration.CategoryId}`, + loadChildren: async () => + (await import('@angular-spotify/web/browse/feature/category')).BrowseCategoryModule + } + ]), + StoreModule.forFeature(categoriesFeatureKey, categoriesReducer), + StoreModule.forFeature(categoryPlaylistsFeatureKey, categoryPlaylistsReducer), + EffectsModule.forFeature([CategoriesEffect, CategoryPlaylistsEffect]) + ] +}) +export class BrowseShellModule {} diff --git a/libs/web/browse/feature/shell/src/test-setup.ts b/libs/web/browse/feature/shell/src/test-setup.ts new file mode 100644 index 00000000..8d88704e --- /dev/null +++ b/libs/web/browse/feature/shell/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular'; diff --git a/libs/web/browse/feature/shell/tsconfig.json b/libs/web/browse/feature/shell/tsconfig.json new file mode 100644 index 00000000..3e77fb66 --- /dev/null +++ b/libs/web/browse/feature/shell/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictTemplates": true + } +} diff --git a/libs/web/browse/feature/shell/tsconfig.lib.json b/libs/web/browse/feature/shell/tsconfig.lib.json new file mode 100644 index 00000000..2c746827 --- /dev/null +++ b/libs/web/browse/feature/shell/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/web/browse/feature/shell/tsconfig.spec.json b/libs/web/browse/feature/shell/tsconfig.spec.json new file mode 100644 index 00000000..3d77fc5c --- /dev/null +++ b/libs/web/browse/feature/shell/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/web/browse/feature/src/index.ts b/libs/web/browse/feature/src/index.ts deleted file mode 100644 index d0ad23da..00000000 --- a/libs/web/browse/feature/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/browse.module'; diff --git a/libs/web/browse/feature/src/lib/browse.component.html b/libs/web/browse/feature/src/lib/browse.component.html deleted file mode 100644 index 88112a97..00000000 --- a/libs/web/browse/feature/src/lib/browse.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
- - -
\ No newline at end of file diff --git a/libs/web/browse/feature/src/lib/browse.component.ts b/libs/web/browse/feature/src/lib/browse.component.ts deleted file mode 100644 index 11fd9bd4..00000000 --- a/libs/web/browse/feature/src/lib/browse.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; - -@Component({ - selector: 'as-browse', - templateUrl: './browse.component.html', - styleUrls: ['./browse.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class BrowseComponent {} diff --git a/libs/web/browse/feature/src/lib/browse.module.ts b/libs/web/browse/feature/src/lib/browse.module.ts deleted file mode 100644 index a75fef85..00000000 --- a/libs/web/browse/feature/src/lib/browse.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { BrowseComponent } from './browse.component'; -import { WorkInProgressModule } from '@angular-spotify/web/shared/ui/work-in-progress'; -import { RouterModule } from '@angular/router'; - -@NgModule({ - imports: [ - CommonModule, - WorkInProgressModule, - RouterModule.forChild([ - { - path: '', - component: BrowseComponent - } - ]) - ], - declarations: [BrowseComponent], - exports: [BrowseComponent] -}) -export class BrowseModule {} diff --git a/libs/web/browse/ui/category-cover/.eslintrc.json b/libs/web/browse/ui/category-cover/.eslintrc.json new file mode 100644 index 00000000..58d3049b --- /dev/null +++ b/libs/web/browse/ui/category-cover/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["../../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nrwl/nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "parserOptions": { "project": ["libs/web/browse/ui/category-cover/tsconfig.*?.json"] }, + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { "type": "attribute", "prefix": "as", "style": "camelCase" } + ], + "@angular-eslint/component-selector": [ + "error", + { "type": "element", "prefix": "as", "style": "kebab-case" } + ] + } + }, + { "files": ["*.html"], "extends": ["plugin:@nrwl/nx/angular-template"], "rules": {} } + ] +} diff --git a/libs/web/browse/ui/category-cover/README.md b/libs/web/browse/ui/category-cover/README.md new file mode 100644 index 00000000..dd3cfb92 --- /dev/null +++ b/libs/web/browse/ui/category-cover/README.md @@ -0,0 +1,7 @@ +# web-browse-ui-category-cover + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test web-browse-ui-category-cover` to execute the unit tests. diff --git a/libs/web/browse/ui/category-cover/jest.config.js b/libs/web/browse/ui/category-cover/jest.config.js new file mode 100644 index 00000000..93f42727 --- /dev/null +++ b/libs/web/browse/ui/category-cover/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + displayName: 'web-browse-ui-category-cover', + preset: '../../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + astTransformers: { + before: [ + 'jest-preset-angular/build/InlineFilesTransformer', + 'jest-preset-angular/build/StripStylesTransformer' + ] + } + } + }, + coverageDirectory: '../../../../../coverage/libs/web/browse/ui/category-cover', + snapshotSerializers: [ + 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', + 'jest-preset-angular/build/AngularSnapshotSerializer.js', + 'jest-preset-angular/build/HTMLCommentSerializer.js' + ] +}; diff --git a/libs/web/browse/ui/category-cover/src/index.ts b/libs/web/browse/ui/category-cover/src/index.ts new file mode 100644 index 00000000..0cd3f757 --- /dev/null +++ b/libs/web/browse/ui/category-cover/src/index.ts @@ -0,0 +1 @@ +export * from './lib/category-cover.module'; diff --git a/libs/web/browse/ui/category-cover/src/lib/category-cover.component.html b/libs/web/browse/ui/category-cover/src/lib/category-cover.component.html new file mode 100644 index 00000000..9040bcf8 --- /dev/null +++ b/libs/web/browse/ui/category-cover/src/lib/category-cover.component.html @@ -0,0 +1,9 @@ + + + + +
+ {{ category.name }} +
+
\ No newline at end of file diff --git a/libs/web/browse/ui/category-cover/src/lib/category-cover.component.scss b/libs/web/browse/ui/category-cover/src/lib/category-cover.component.scss new file mode 100644 index 00000000..a6282466 --- /dev/null +++ b/libs/web/browse/ui/category-cover/src/lib/category-cover.component.scss @@ -0,0 +1,17 @@ +.category-cover-container { + display: flex; + position: relative; + &:hover { + .category-name { + @apply underline; + } + } +} + +.category-name { + position: absolute; + bottom: 40px; + left: 50%; + transform: translateX(-50%); + @apply text-white text-base; +} diff --git a/libs/web/browse/ui/category-cover/src/lib/category-cover.component.ts b/libs/web/browse/ui/category-cover/src/lib/category-cover.component.ts new file mode 100644 index 00000000..76966cf5 --- /dev/null +++ b/libs/web/browse/ui/category-cover/src/lib/category-cover.component.ts @@ -0,0 +1,11 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + selector: 'as-category-cover', + templateUrl: './category-cover.component.html', + styleUrls: ['./category-cover.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CategoryCoverComponent { + @Input() category!: SpotifyApi.CategoryObject; +} diff --git a/libs/web/browse/ui/category-cover/src/lib/category-cover.module.ts b/libs/web/browse/ui/category-cover/src/lib/category-cover.module.ts new file mode 100644 index 00000000..3957608a --- /dev/null +++ b/libs/web/browse/ui/category-cover/src/lib/category-cover.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CategoryCoverComponent } from './category-cover.component'; +import { MediaCoverModule } from '@angular-spotify/web/shared/ui/media-cover'; +import { RouterModule } from '@angular/router'; + +@NgModule({ + imports: [CommonModule, MediaCoverModule, RouterModule], + declarations: [CategoryCoverComponent], + exports: [CategoryCoverComponent] +}) +export class CategoryCoverModule {} diff --git a/libs/web/browse/ui/category-cover/src/test-setup.ts b/libs/web/browse/ui/category-cover/src/test-setup.ts new file mode 100644 index 00000000..8d88704e --- /dev/null +++ b/libs/web/browse/ui/category-cover/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular'; diff --git a/libs/web/browse/ui/category-cover/tsconfig.json b/libs/web/browse/ui/category-cover/tsconfig.json new file mode 100644 index 00000000..3e77fb66 --- /dev/null +++ b/libs/web/browse/ui/category-cover/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictTemplates": true + } +} diff --git a/libs/web/browse/ui/category-cover/tsconfig.lib.json b/libs/web/browse/ui/category-cover/tsconfig.lib.json new file mode 100644 index 00000000..92385318 --- /dev/null +++ b/libs/web/browse/ui/category-cover/tsconfig.lib.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/web/browse/ui/category-cover/tsconfig.spec.json b/libs/web/browse/ui/category-cover/tsconfig.spec.json new file mode 100644 index 00000000..3d77fc5c --- /dev/null +++ b/libs/web/browse/ui/category-cover/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/web/home/ui/featured-playlists/src/lib/featured-playlists.component.html b/libs/web/home/ui/featured-playlists/src/lib/featured-playlists.component.html index 56242d8a..127be6c5 100644 --- a/libs/web/home/ui/featured-playlists/src/lib/featured-playlists.component.html +++ b/libs/web/home/ui/featured-playlists/src/lib/featured-playlists.component.html @@ -1,15 +1,13 @@ -

{{ data.message }}

+

{{ data.message }}

- +
-
+ \ No newline at end of file diff --git a/libs/web/home/ui/recent-played/src/lib/recent-played.component.html b/libs/web/home/ui/recent-played/src/lib/recent-played.component.html index 243d67f3..fbe2a0de 100644 --- a/libs/web/home/ui/recent-played/src/lib/recent-played.component.html +++ b/libs/web/home/ui/recent-played/src/lib/recent-played.component.html @@ -1,4 +1,4 @@ -

Recently Played

+

Recently Played

@@ -11,8 +11,5 @@

Recently Played

(togglePlay)="togglePlayTrack($event, historyObject.track.uri)">
-
- -
\ No newline at end of file + + \ No newline at end of file diff --git a/libs/web/home/ui/recent-played/src/lib/recent-played.module.ts b/libs/web/home/ui/recent-played/src/lib/recent-played.module.ts index b9a234cc..6858e5b4 100644 --- a/libs/web/home/ui/recent-played/src/lib/recent-played.module.ts +++ b/libs/web/home/ui/recent-played/src/lib/recent-played.module.ts @@ -2,9 +2,9 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RecentPlayedComponent } from './recent-played.component'; import { MediaModule } from '@angular-spotify/web/shared/ui/media'; -import { SvgIconsModule } from '@ngneat/svg-icon'; +import { SpinnerModule } from '@angular-spotify/web/shared/ui/spinner'; @NgModule({ - imports: [CommonModule, MediaModule, SvgIconsModule], + imports: [CommonModule, MediaModule, SpinnerModule], declarations: [RecentPlayedComponent], exports: [RecentPlayedComponent] }) diff --git a/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.action.ts b/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.action.ts index f52c6f22..645d99ff 100644 --- a/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.action.ts +++ b/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.action.ts @@ -16,7 +16,7 @@ export const loadPlaylistTracksError = createAction( props<{ error: string }>() ); -export const statePlaylistTracksStateStatus = createAction( +export const setPlaylistTracksStateStatus = createAction( '[Playlist Tracks/Set Playlist Tracks Status]', props<{ status: GenericStoreStatus }>() ); diff --git a/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.effect.ts b/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.effect.ts index 3ca85250..0d4b21dc 100644 --- a/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.effect.ts +++ b/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.effect.ts @@ -8,7 +8,7 @@ import { getPlaylistTracksState } from './playlist-tracks.selector'; import { loadPlaylistTracks, loadPlaylistTracksSuccess, - statePlaylistTracksStateStatus + setPlaylistTracksStateStatus } from './playlist-tracks.action'; @Injectable({ providedIn: 'root' }) @@ -20,7 +20,7 @@ export class PlaylistTracksEffect { tap(([{ playlistId }, playlistTracks]) => { if (playlistTracks.data?.has(playlistId)) { this.store.dispatch( - statePlaylistTracksStateStatus({ + setPlaylistTracksStateStatus({ status: 'success' }) ); diff --git a/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.reducer.ts b/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.reducer.ts index cf5f0e5d..bde8e46b 100644 --- a/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.reducer.ts +++ b/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.reducer.ts @@ -4,7 +4,7 @@ import { loadPlaylistTracks, loadPlaylistTracksError, loadPlaylistTracksSuccess, - statePlaylistTracksStateStatus + setPlaylistTracksStateStatus } from './playlist-tracks.action'; export const playlistTrackFeatureKey = 'playlistTracks'; @@ -25,5 +25,5 @@ export const playlistTracksReducer = createReducer( return { ...state, data: map, status: 'success' }; }), on(loadPlaylistTracksError, (state, { error }) => ({ ...state, error, status: 'error' })), - on(statePlaylistTracksStateStatus, (state, { status }) => ({ ...state, status })) + on(setPlaylistTracksStateStatus, (state, { status }) => ({ ...state, status })) ); diff --git a/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.selector.ts b/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.selector.ts index 3465e3c3..87c5a2a2 100644 --- a/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.selector.ts +++ b/libs/web/playlist/data-access/src/lib/store/playlist-tracks/playlist-tracks.selector.ts @@ -11,8 +11,6 @@ export const getPlaylistTracksLoading = createSelector( SelectorUtil.isLoading ); -export const getPlaylistTracksDone = createSelector(getPlaylistTracksState, SelectorUtil.isDone); - export const getPlaylistTracksById = (playlistId: string) => createSelector(getPlaylistTracksState, ({ data }) => { return data?.get(playlistId); diff --git a/libs/web/playlist/data-access/src/lib/store/playlists/playlists.selector.ts b/libs/web/playlist/data-access/src/lib/store/playlists/playlists.selector.ts index e6258189..2d1fa1ed 100644 --- a/libs/web/playlist/data-access/src/lib/store/playlists/playlists.selector.ts +++ b/libs/web/playlist/data-access/src/lib/store/playlists/playlists.selector.ts @@ -1,23 +1,14 @@ -import { RouteUtil, SelectorUtil } from '@angular-spotify/web/shared/utils'; +import { SelectorUtil } from '@angular-spotify/web/shared/utils'; import { createFeatureSelector, createSelector } from '@ngrx/store'; import { playlistsFeatureKey, PlaylistsState } from './playlists.reducer'; export const getPlaylistsState = createFeatureSelector(playlistsFeatureKey); export const getPlaylists = createSelector(getPlaylistsState, (state) => state.data); -export const getPlaylistsWithRouteUrl = createSelector(getPlaylists, (playlists) => { - if (playlists) { - return { - ...playlists, - items: playlists.items.map((item) => ({ - ...item, - routeUrl: RouteUtil.getPlaylistRouteUrl(item) - })) - }; - } - return playlists; -}); +export const getPlaylistsWithRouteUrl = createSelector( + getPlaylists, + SelectorUtil.getPlaylistsWithRoute +); export const getPlaylistsLoading = createSelector(getPlaylistsState, SelectorUtil.isLoading); -export const getPlaylistsDone = createSelector(getPlaylistsState, SelectorUtil.isDone); export const getPlaylistsMap = createSelector(getPlaylistsState, (state) => state.map); export const getPlaylist = (playlistId: string) => createSelector(getPlaylistsMap, (map) => map?.get(playlistId)); diff --git a/libs/web/playlist/feature/list/src/lib/playlists.component.html b/libs/web/playlist/feature/list/src/lib/playlists.component.html index 07f11810..0f13b659 100644 --- a/libs/web/playlist/feature/list/src/lib/playlists.component.html +++ b/libs/web/playlist/feature/list/src/lib/playlists.component.html @@ -1,17 +1,6 @@
-
- - -
-
- -
-
+ + + + \ No newline at end of file diff --git a/libs/web/playlist/feature/list/src/lib/playlists.component.scss b/libs/web/playlist/feature/list/src/lib/playlists.component.scss index bc9d7c67..5d4e87f3 100644 --- a/libs/web/playlist/feature/list/src/lib/playlists.component.scss +++ b/libs/web/playlist/feature/list/src/lib/playlists.component.scss @@ -1,9 +1,3 @@ :host { display: block; } - -.playlist-container { - gap: 24px; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - @apply grid; -} diff --git a/libs/web/playlist/feature/list/src/lib/playlists.component.ts b/libs/web/playlist/feature/list/src/lib/playlists.component.ts index db61df6f..c8feced9 100644 --- a/libs/web/playlist/feature/list/src/lib/playlists.component.ts +++ b/libs/web/playlist/feature/list/src/lib/playlists.component.ts @@ -1,8 +1,9 @@ -import { PlayerApiService } from '@angular-spotify/web/shared/data-access/spotify-api'; -import { RouteUtil } from '@angular-spotify/web/shared/utils'; +import { + getPlaylistsLoading, + getPlaylistsWithRouteUrl +} from '@angular-spotify/web/playlist/data-access'; import { ChangeDetectionStrategy, Component } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { getPlaylists, getPlaylistsLoading, getPlaylistsWithRouteUrl } from '@angular-spotify/web/playlist/data-access'; @Component({ selector: 'as-playlists', @@ -14,14 +15,5 @@ export class PlaylistsComponent { playlists$ = this.store.pipe(select(getPlaylistsWithRouteUrl)); isPlaylistsLoading$ = this.store.pipe(select(getPlaylistsLoading)); - constructor(private store: Store, private playerApi: PlayerApiService) { - } - - togglePlay(isPlaying: boolean, contextUri: string) { - this.playerApi - .togglePlay(isPlaying, { - context_uri: contextUri - }) - .subscribe(); - } + constructor(private store: Store) {} } diff --git a/libs/web/playlist/feature/list/src/lib/playlists.module.ts b/libs/web/playlist/feature/list/src/lib/playlists.module.ts index edb9c0e7..be537b57 100644 --- a/libs/web/playlist/feature/list/src/lib/playlists.module.ts +++ b/libs/web/playlist/feature/list/src/lib/playlists.module.ts @@ -2,21 +2,19 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Route } from '@angular/router'; import { PlaylistsComponent } from './playlists.component'; -import { MediaModule } from '@angular-spotify/web/shared/ui/media'; -import { SvgIconsModule } from '@ngneat/svg-icon'; +import { PlaylistListModule } from '@angular-spotify/web/shared/ui/playlist-list'; export const playlistsRoutes: Route[] = []; @NgModule({ imports: [ CommonModule, - MediaModule, RouterModule.forChild([ { path: '', component: PlaylistsComponent } ]), - SvgIconsModule + PlaylistListModule ], declarations: [PlaylistsComponent], exports: [PlaylistsComponent] diff --git a/libs/web/shared/data-access/models/src/lib/api/index.ts b/libs/web/shared/data-access/models/src/lib/api/index.ts index a85943e0..12d56990 100644 --- a/libs/web/shared/data-access/models/src/lib/api/index.ts +++ b/libs/web/shared/data-access/models/src/lib/api/index.ts @@ -1,3 +1,4 @@ export * from './play-request'; export * from './user-recent-played-track'; export * from './audio-analysis-response'; +export * from './playlists-with-route'; diff --git a/libs/web/shared/data-access/models/src/lib/api/playlists-with-route.ts b/libs/web/shared/data-access/models/src/lib/api/playlists-with-route.ts new file mode 100644 index 00000000..861ed153 --- /dev/null +++ b/libs/web/shared/data-access/models/src/lib/api/playlists-with-route.ts @@ -0,0 +1,5 @@ +export type PlaylistWithRouteUrl = SpotifyApi.PlaylistObjectSimplified & { + routeUrl: string; +}; + +export type PlaylistsResponseWithRoute = SpotifyApi.PagingObject; diff --git a/libs/web/shared/data-access/models/src/lib/nav-item.ts b/libs/web/shared/data-access/models/src/lib/nav-item.ts index f8533435..f8367778 100644 --- a/libs/web/shared/data-access/models/src/lib/nav-item.ts +++ b/libs/web/shared/data-access/models/src/lib/nav-item.ts @@ -2,4 +2,5 @@ export interface NavItem { label: string; path: string; icon?: string; + exact?: boolean; } diff --git a/libs/web/shared/data-access/spotify-api/src/lib/browse-api.ts b/libs/web/shared/data-access/spotify-api/src/lib/browse-api.ts index 8ff6df05..01572e1a 100644 --- a/libs/web/shared/data-access/spotify-api/src/lib/browse-api.ts +++ b/libs/web/shared/data-access/spotify-api/src/lib/browse-api.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { AppConfig, APP_CONFIG } from '@angular-spotify/web/shared/app-config'; - +import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class BrowseApiService { browseUrl: string; @@ -10,7 +10,8 @@ export class BrowseApiService { } getAllFeaturedPlaylists( - params: Record = {// eslint-disable-line + params: Record = { + // eslint-disable-line limit: 50 } ) { @@ -21,4 +22,34 @@ export class BrowseApiService { } ); } + + getAllCategories( + params: Record = { + // eslint-disable-line + limit: 50 + } + ) { + return this.http + .get(`${this.browseUrl}/categories`, { + params + }) + .pipe(map((res) => res.categories)); + } + + getCategoryPlaylists( + categoryId: string, + params: Record = { + // eslint-disable-line + limit: 50 + } + ) { + return this.http + .get( + `${this.browseUrl}/categories/${categoryId}/playlists`, + { + params + } + ) + .pipe(map((res) => res.playlists)); + } } diff --git a/libs/web/shared/data-access/store/src/lib/ui/ui-store.ts b/libs/web/shared/data-access/store/src/lib/ui/ui-store.ts index 072e158a..0a3578b1 100644 --- a/libs/web/shared/data-access/store/src/lib/ui/ui-store.ts +++ b/libs/web/shared/data-access/store/src/lib/ui/ui-store.ts @@ -15,7 +15,7 @@ export class UIStore extends ComponentStore { constructor(private modalService: NzModalService) { super({ navItems: [ - { label: 'Home', path: '' }, + { label: 'Home', path: '', exact: true }, { label: 'Browse', path: '/browse' }, { label: 'Your Library', path: '/collection/playlists' } ], diff --git a/libs/web/shared/ui/media-cover/src/lib/media-cover.component.ts b/libs/web/shared/ui/media-cover/src/lib/media-cover.component.ts index e5ff71a1..5f63945e 100644 --- a/libs/web/shared/ui/media-cover/src/lib/media-cover.component.ts +++ b/libs/web/shared/ui/media-cover/src/lib/media-cover.component.ts @@ -7,10 +7,9 @@ import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular changeDetection: ChangeDetectionStrategy.OnPush }) export class MediaCoverComponent { - @Input() imageUrl: string | undefined; - - @HostBinding('style.background-image') - get backgroundUrl() { - return `url(${this.imageUrl})`; + @Input() set imageUrl(url: string | undefined) { + this.backgroundUrl = url && `url(${url})`; } + + @HostBinding('style.background-image') backgroundUrl!: string | undefined; } diff --git a/libs/web/shared/ui/playlist-list/.eslintrc.json b/libs/web/shared/ui/playlist-list/.eslintrc.json new file mode 100644 index 00000000..e403604b --- /dev/null +++ b/libs/web/shared/ui/playlist-list/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["../../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nrwl/nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "parserOptions": { "project": ["libs/web/shared/ui/playlist-list/tsconfig.*?.json"] }, + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { "type": "attribute", "prefix": "as", "style": "camelCase" } + ], + "@angular-eslint/component-selector": [ + "error", + { "type": "element", "prefix": "as", "style": "kebab-case" } + ] + } + }, + { "files": ["*.html"], "extends": ["plugin:@nrwl/nx/angular-template"], "rules": {} } + ] +} diff --git a/libs/web/shared/ui/playlist-list/README.md b/libs/web/shared/ui/playlist-list/README.md new file mode 100644 index 00000000..1d63a30d --- /dev/null +++ b/libs/web/shared/ui/playlist-list/README.md @@ -0,0 +1,7 @@ +# web-shared-ui-playlist-list + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test web-shared-ui-playlist-list` to execute the unit tests. diff --git a/libs/web/shared/ui/playlist-list/jest.config.js b/libs/web/shared/ui/playlist-list/jest.config.js new file mode 100644 index 00000000..196c1cd6 --- /dev/null +++ b/libs/web/shared/ui/playlist-list/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + displayName: 'web-shared-ui-playlist-list', + preset: '../../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + astTransformers: { + before: [ + 'jest-preset-angular/build/InlineFilesTransformer', + 'jest-preset-angular/build/StripStylesTransformer' + ] + } + } + }, + coverageDirectory: '../../../../../coverage/libs/web/shared/ui/playlist-list', + snapshotSerializers: [ + 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', + 'jest-preset-angular/build/AngularSnapshotSerializer.js', + 'jest-preset-angular/build/HTMLCommentSerializer.js' + ] +}; diff --git a/libs/web/shared/ui/playlist-list/src/index.ts b/libs/web/shared/ui/playlist-list/src/index.ts new file mode 100644 index 00000000..73ef4f9c --- /dev/null +++ b/libs/web/shared/ui/playlist-list/src/index.ts @@ -0,0 +1 @@ +export * from './lib/playlist-list.module'; diff --git a/libs/web/shared/ui/playlist-list/src/lib/playlist-list.component.html b/libs/web/shared/ui/playlist-list/src/lib/playlist-list.component.html new file mode 100644 index 00000000..aeaed034 --- /dev/null +++ b/libs/web/shared/ui/playlist-list/src/lib/playlist-list.component.html @@ -0,0 +1,13 @@ +
+ + +
+ + \ No newline at end of file diff --git a/libs/web/shared/ui/playlist-list/src/lib/playlist-list.component.scss b/libs/web/shared/ui/playlist-list/src/lib/playlist-list.component.scss new file mode 100644 index 00000000..5d4e87f3 --- /dev/null +++ b/libs/web/shared/ui/playlist-list/src/lib/playlist-list.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/libs/web/shared/ui/playlist-list/src/lib/playlist-list.component.ts b/libs/web/shared/ui/playlist-list/src/lib/playlist-list.component.ts new file mode 100644 index 00000000..d3c1c3be --- /dev/null +++ b/libs/web/shared/ui/playlist-list/src/lib/playlist-list.component.ts @@ -0,0 +1,24 @@ +import { PlaylistsResponseWithRoute } from '@angular-spotify/web/shared/data-access/models'; +import { PlayerApiService } from '@angular-spotify/web/shared/data-access/spotify-api'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + selector: 'as-playlist-list', + templateUrl: './playlist-list.component.html', + styleUrls: ['./playlist-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PlaylistListComponent { + @Input() playlists!: PlaylistsResponseWithRoute | null; + @Input() isLoading!: boolean | null; + + constructor(private playerApi: PlayerApiService) {} + + togglePlay(isPlaying: boolean, contextUri: string) { + this.playerApi + .togglePlay(isPlaying, { + context_uri: contextUri + }) + .subscribe(); + } +} diff --git a/libs/web/shared/ui/playlist-list/src/lib/playlist-list.module.ts b/libs/web/shared/ui/playlist-list/src/lib/playlist-list.module.ts new file mode 100644 index 00000000..75d5e4bf --- /dev/null +++ b/libs/web/shared/ui/playlist-list/src/lib/playlist-list.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PlaylistListComponent } from './playlist-list.component'; +import { MediaModule } from '@angular-spotify/web/shared/ui/media'; +import { SpinnerModule } from '@angular-spotify/web/shared/ui/spinner'; + +@NgModule({ + imports: [CommonModule, MediaModule, SpinnerModule], + declarations: [PlaylistListComponent], + exports: [PlaylistListComponent] +}) +export class PlaylistListModule {} diff --git a/libs/web/shared/ui/playlist-list/src/test-setup.ts b/libs/web/shared/ui/playlist-list/src/test-setup.ts new file mode 100644 index 00000000..8d88704e --- /dev/null +++ b/libs/web/shared/ui/playlist-list/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular'; diff --git a/libs/web/shared/ui/playlist-list/tsconfig.json b/libs/web/shared/ui/playlist-list/tsconfig.json new file mode 100644 index 00000000..3e77fb66 --- /dev/null +++ b/libs/web/shared/ui/playlist-list/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictTemplates": true + } +} diff --git a/libs/web/shared/ui/playlist-list/tsconfig.lib.json b/libs/web/shared/ui/playlist-list/tsconfig.lib.json new file mode 100644 index 00000000..2c746827 --- /dev/null +++ b/libs/web/shared/ui/playlist-list/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/web/shared/ui/playlist-list/tsconfig.spec.json b/libs/web/shared/ui/playlist-list/tsconfig.spec.json new file mode 100644 index 00000000..3d77fc5c --- /dev/null +++ b/libs/web/shared/ui/playlist-list/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/web/browse/feature/.eslintrc.json b/libs/web/shared/ui/spinner/.eslintrc.json similarity index 82% rename from libs/web/browse/feature/.eslintrc.json rename to libs/web/shared/ui/spinner/.eslintrc.json index e92b3f60..7019106b 100644 --- a/libs/web/browse/feature/.eslintrc.json +++ b/libs/web/shared/ui/spinner/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": ["../../../../.eslintrc.json"], + "extends": ["../../../../../.eslintrc.json"], "ignorePatterns": ["!**/*"], "overrides": [ { @@ -8,7 +8,7 @@ "plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates" ], - "parserOptions": { "project": ["libs/web/browse/feature/tsconfig.*?.json"] }, + "parserOptions": { "project": ["libs/web/shared/ui/spinner/tsconfig.*?.json"] }, "rules": { "@angular-eslint/directive-selector": [ "error", diff --git a/libs/web/shared/ui/spinner/README.md b/libs/web/shared/ui/spinner/README.md new file mode 100644 index 00000000..70af539e --- /dev/null +++ b/libs/web/shared/ui/spinner/README.md @@ -0,0 +1,7 @@ +# web-shared-ui-spinner + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test web-shared-ui-spinner` to execute the unit tests. diff --git a/libs/web/browse/feature/jest.config.js b/libs/web/shared/ui/spinner/jest.config.js similarity index 79% rename from libs/web/browse/feature/jest.config.js rename to libs/web/shared/ui/spinner/jest.config.js index 46c8bd77..fc38243f 100644 --- a/libs/web/browse/feature/jest.config.js +++ b/libs/web/shared/ui/spinner/jest.config.js @@ -1,6 +1,6 @@ module.exports = { - displayName: 'web-browse-feature', - preset: '../../../../jest.preset.js', + displayName: 'web-shared-ui-spinner', + preset: '../../../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { 'ts-jest': { @@ -14,7 +14,7 @@ module.exports = { } } }, - coverageDirectory: '../../../../coverage/libs/web/browse/feature', + coverageDirectory: '../../../../../coverage/libs/web/shared/ui/spinner', snapshotSerializers: [ 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 'jest-preset-angular/build/AngularSnapshotSerializer.js', diff --git a/libs/web/shared/ui/spinner/src/index.ts b/libs/web/shared/ui/spinner/src/index.ts new file mode 100644 index 00000000..669b19b9 --- /dev/null +++ b/libs/web/shared/ui/spinner/src/index.ts @@ -0,0 +1 @@ +export * from './lib/spinner.module'; diff --git a/libs/web/shared/ui/spinner/src/lib/spinner.component.html b/libs/web/shared/ui/spinner/src/lib/spinner.component.html new file mode 100644 index 00000000..67627d94 --- /dev/null +++ b/libs/web/shared/ui/spinner/src/lib/spinner.component.html @@ -0,0 +1,4 @@ +
+ +
\ No newline at end of file diff --git a/libs/web/shared/ui/spinner/src/lib/spinner.component.scss b/libs/web/shared/ui/spinner/src/lib/spinner.component.scss new file mode 100644 index 00000000..5d4e87f3 --- /dev/null +++ b/libs/web/shared/ui/spinner/src/lib/spinner.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/libs/web/shared/ui/spinner/src/lib/spinner.component.ts b/libs/web/shared/ui/spinner/src/lib/spinner.component.ts new file mode 100644 index 00000000..d7a337bb --- /dev/null +++ b/libs/web/shared/ui/spinner/src/lib/spinner.component.ts @@ -0,0 +1,12 @@ +import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; +import { SVG_CONFIG } from '@ngneat/svg-icon/lib/types'; + +@Component({ + selector: 'as-spinner', + templateUrl: './spinner.component.html', + styleUrls: ['./spinner.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SpinnerComponent { + @Input() size: keyof SVG_CONFIG['sizes'] = 'xl'; +} diff --git a/libs/web/shared/ui/spinner/src/lib/spinner.module.ts b/libs/web/shared/ui/spinner/src/lib/spinner.module.ts new file mode 100644 index 00000000..a76aef82 --- /dev/null +++ b/libs/web/shared/ui/spinner/src/lib/spinner.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SpinnerComponent } from './spinner.component'; +import {SvgIconsModule} from '@ngneat/svg-icon'; + +@NgModule({ + imports: [CommonModule, SvgIconsModule], + declarations: [SpinnerComponent], + exports: [SpinnerComponent] +}) +export class SpinnerModule {} diff --git a/libs/web/shared/ui/spinner/src/test-setup.ts b/libs/web/shared/ui/spinner/src/test-setup.ts new file mode 100644 index 00000000..8d88704e --- /dev/null +++ b/libs/web/shared/ui/spinner/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular'; diff --git a/libs/web/shared/ui/spinner/tsconfig.json b/libs/web/shared/ui/spinner/tsconfig.json new file mode 100644 index 00000000..3e77fb66 --- /dev/null +++ b/libs/web/shared/ui/spinner/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictTemplates": true + } +} diff --git a/libs/web/shared/ui/spinner/tsconfig.lib.json b/libs/web/shared/ui/spinner/tsconfig.lib.json new file mode 100644 index 00000000..2c746827 --- /dev/null +++ b/libs/web/shared/ui/spinner/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/web/shared/ui/spinner/tsconfig.spec.json b/libs/web/shared/ui/spinner/tsconfig.spec.json new file mode 100644 index 00000000..3d77fc5c --- /dev/null +++ b/libs/web/shared/ui/spinner/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/web/shared/utils/src/lib/router-util.ts b/libs/web/shared/utils/src/lib/router-util.ts index 2fb33eb5..8e0a612f 100644 --- a/libs/web/shared/utils/src/lib/router-util.ts +++ b/libs/web/shared/utils/src/lib/router-util.ts @@ -6,6 +6,8 @@ export class RouterUtil { AlbumId: 'albumId', Artist: 'artist', ArtistId: 'artistId', - Visualizer: 'visualizer' + Visualizer: 'visualizer', + Browse: 'browse', + CategoryId: 'categoryId' }; } diff --git a/libs/web/shared/utils/src/lib/selector-util.ts b/libs/web/shared/utils/src/lib/selector-util.ts index 59692004..59601400 100644 --- a/libs/web/shared/utils/src/lib/selector-util.ts +++ b/libs/web/shared/utils/src/lib/selector-util.ts @@ -1,7 +1,11 @@ /// -import { GenericState } from '@angular-spotify/web/shared/data-access/models'; +import { + GenericState, + PlaylistsResponseWithRoute +} from '@angular-spotify/web/shared/data-access/models'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; +import { RouteUtil } from './route-util'; export class SelectorUtil { static getMediaPlayingState(obs$: Observable<[string | undefined, Spotify.PlaybackState]>) { return obs$.pipe( @@ -49,4 +53,19 @@ export class SelectorUtil { static isDone({ status }: GenericState) { return status === 'success' || status === 'error'; } + + static getPlaylistsWithRoute( + playlists: SpotifyApi.ListOfUsersPlaylistsResponse | null | undefined + ): PlaylistsResponseWithRoute | null | undefined { + if (playlists) { + return { + ...playlists, + items: playlists.items.map((item) => ({ + ...item, + routeUrl: RouteUtil.getPlaylistRouteUrl(item) + })) + }; + } + return playlists; + } } diff --git a/libs/web/shell/feature/src/lib/web-shell.routes.ts b/libs/web/shell/feature/src/lib/web-shell.routes.ts index 39f85211..d9815e56 100644 --- a/libs/web/shell/feature/src/lib/web-shell.routes.ts +++ b/libs/web/shell/feature/src/lib/web-shell.routes.ts @@ -13,7 +13,8 @@ export const webShellRoutes: Route[] = [ }, { path: 'browse', - loadChildren: async () => (await import('@angular-spotify/web/browse/feature')).BrowseModule + loadChildren: async () => + (await import('@angular-spotify/web/browse/feature/shell')).BrowseShellModule }, { path: 'collection/playlists', diff --git a/libs/web/shell/ui/nav-bar-playlist/src/lib/nav-bar-playlist.component.html b/libs/web/shell/ui/nav-bar-playlist/src/lib/nav-bar-playlist.component.html index f1f276f5..5c5721cd 100644 --- a/libs/web/shell/ui/nav-bar-playlist/src/lib/nav-bar-playlist.component.html +++ b/libs/web/shell/ui/nav-bar-playlist/src/lib/nav-bar-playlist.component.html @@ -1,21 +1,21 @@

Playlists

-
    -
  • +
      +
    -
    - -
    + + \ No newline at end of file diff --git a/libs/web/shell/ui/nav-bar-playlist/src/lib/nav-bar-playlist.module.ts b/libs/web/shell/ui/nav-bar-playlist/src/lib/nav-bar-playlist.module.ts index 9c5860ad..e52b3261 100644 --- a/libs/web/shell/ui/nav-bar-playlist/src/lib/nav-bar-playlist.module.ts +++ b/libs/web/shell/ui/nav-bar-playlist/src/lib/nav-bar-playlist.module.ts @@ -5,9 +5,9 @@ import { RouterModule } from '@angular/router'; import { NavPlaylistComponent } from './nav-playlist/nav-playlist.component'; import { PlayButtonModule } from '@angular-spotify/web/shared/ui/play-button'; import { ReactiveComponentModule } from '@ngrx/component'; -import { SvgIconsModule } from '@ngneat/svg-icon'; +import { SpinnerModule } from '@angular-spotify/web/shared/ui/spinner'; @NgModule({ - imports: [CommonModule, RouterModule, PlayButtonModule, ReactiveComponentModule, SvgIconsModule], + imports: [CommonModule, RouterModule, PlayButtonModule, ReactiveComponentModule, SpinnerModule], declarations: [NavBarPlaylistComponent, NavPlaylistComponent], exports: [NavBarPlaylistComponent] }) diff --git a/libs/web/shell/ui/nav-bar/src/lib/nav-bar.component.html b/libs/web/shell/ui/nav-bar/src/lib/nav-bar.component.html index a7fb8a3b..c274db96 100644 --- a/libs/web/shell/ui/nav-bar/src/lib/nav-bar.component.html +++ b/libs/web/shell/ui/nav-bar/src/lib/nav-bar.component.html @@ -1,15 +1,15 @@ - +