feat: search for books
This commit is contained in:
parent
6aae6b14dd
commit
4f75e1a2ea
22 changed files with 426 additions and 6 deletions
27
nx.json
27
nx.json
|
|
@ -4,17 +4,31 @@
|
|||
"default": {
|
||||
"runner": "nx/tasks-runners/default",
|
||||
"options": {
|
||||
"cacheableOperations": ["build", "lint", "test", "e2e"]
|
||||
"cacheableOperations": [
|
||||
"build",
|
||||
"lint",
|
||||
"test",
|
||||
"e2e"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["production", "^production"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"production",
|
||||
"^production"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
|
||||
"inputs": [
|
||||
"default",
|
||||
"^production",
|
||||
"{workspaceRoot}/jest.preset.js"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"inputs": [
|
||||
|
|
@ -26,7 +40,10 @@
|
|||
}
|
||||
},
|
||||
"namedInputs": {
|
||||
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
||||
"default": [
|
||||
"{projectRoot}/**/*",
|
||||
"sharedGlobals"
|
||||
],
|
||||
"production": [
|
||||
"default",
|
||||
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"part1": "nx run part-1-html-css:serve"
|
||||
"part1": "nx run part-1-html-css:serve",
|
||||
"part2": "nx run part-2-javascript:serve"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
|
|
|
|||
33
part-2-javascript/.eslintrc.json
Normal file
33
part-2-javascript/.eslintrc.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"extends": [
|
||||
"../.eslintrc.json"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
8
part-2-javascript/.swcrc
Normal file
8
part-2-javascript/.swcrc
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript"
|
||||
},
|
||||
"target": "es2016"
|
||||
}
|
||||
}
|
||||
11
part-2-javascript/jest.config.ts
Normal file
11
part-2-javascript/jest.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'part-2-javascript',
|
||||
preset: '../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': '@swc/jest'
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../coverage/part-2-javascript'
|
||||
};
|
||||
86
part-2-javascript/project.json
Normal file
86
part-2-javascript/project.json
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"name": "part-2-javascript",
|
||||
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "part-2-javascript/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"outputPath": "dist/part-2-javascript",
|
||||
"compiler": "swc",
|
||||
"main": "part-2-javascript/src/main.ts",
|
||||
"tsConfig": "part-2-javascript/tsconfig.app.json",
|
||||
"webpackConfig": "part-2-javascript/webpack.config.js",
|
||||
"assets": [
|
||||
"part-2-javascript/src/assets"
|
||||
],
|
||||
"index": "part-2-javascript/src/index.html",
|
||||
"baseHref": "/",
|
||||
"styles": [
|
||||
"part-2-javascript/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "part-2-javascript/src/environments/environment.ts",
|
||||
"with": "part-2-javascript/src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/webpack:dev-server",
|
||||
"options": {
|
||||
"buildTarget": "part-2-javascript:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "part-2-javascript:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/linter:eslint",
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
],
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"part-2-javascript/**/*.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": [
|
||||
"{workspaceRoot}/coverage/{projectRoot}"
|
||||
],
|
||||
"options": {
|
||||
"jestConfig": "part-2-javascript/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"codeCoverage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
part-2-javascript/src/app/app.element.scss
Normal file
47
part-2-javascript/src/app/app.element.scss
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Remove template code below
|
||||
*/
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
line-height: 1.5;
|
||||
tab-size: 4;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
21
part-2-javascript/src/app/app.element.spec.ts
Normal file
21
part-2-javascript/src/app/app.element.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { AppElement } from './app.element';
|
||||
|
||||
describe('AppElement', () => {
|
||||
let app: AppElement;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new AppElement();
|
||||
});
|
||||
|
||||
it('should create successfully', () => {
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a greeting', () => {
|
||||
app.connectedCallback();
|
||||
|
||||
expect(app.querySelector('h1').innerHTML).toContain(
|
||||
'Welcome part-2-javascript'
|
||||
);
|
||||
});
|
||||
});
|
||||
88
part-2-javascript/src/app/app.element.ts
Normal file
88
part-2-javascript/src/app/app.element.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import "./app.element.scss";
|
||||
import { Book } from "./book.interface";
|
||||
|
||||
export class AppElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div class="container mx-auto sm:px-6 lg:px-8">
|
||||
<!-- Search -->
|
||||
<label for="search" class="block text-sm font-medium leading-6 text-gray-900">Quick search</label>
|
||||
<div class="relative mt-2 flex items-center">
|
||||
<input type="text" name="search" id="search" class="block w-full rounded-md border-0 py-1.5 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" data-search>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-500 py-6">Books</h2>
|
||||
<ul role="list" class="mt-3 grid grid-cols-1 gap-5 sm:grid-cols-2 sm:gap-6 lg:grid-cols-4" data-book-cards-container></ul>
|
||||
|
||||
<template data-book-card-template>
|
||||
<li class="col-span-1 flex rounded-md shadow-sm">
|
||||
<div class="flex w-16 flex-shrink-0 items-center justify-center bg-[#9BD83A] rounded-l-md text-sm font-medium text-gray-900 hover:text-gray-600" data-initials></div>
|
||||
<div class="flex flex-1 items-center justify-between truncate rounded-r-md border-b border-r border-t border-gray-200 bg-white hover:text-gray-600">
|
||||
<div class="flex-1 truncate px-4 py-2 text-sm">
|
||||
<a href="#" class="font-medium text-gray-900 hover:text-gray-600" data-title></a><br>
|
||||
<a href="#" class="font-medium text-gray-900 hover:text-gray-600" data-author></a>
|
||||
<p class="text-gray-500" data-year></p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
const searchInput = this.querySelector("[data-search]");
|
||||
const bookCardTemplate: any = this.querySelector("[data-book-card-template]");
|
||||
const bookCardContainer: HTMLDivElement = this.querySelector("[data-book-cards-container]");
|
||||
|
||||
let books = [];
|
||||
|
||||
fetch("assets/books.json")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
books = data.map((book: Book) => {
|
||||
const card = bookCardTemplate.content.cloneNode(true).children[0];
|
||||
const initials = card.querySelector("[data-initials]");
|
||||
const title = card.querySelector("[data-title]");
|
||||
const author = card.querySelector("[data-author]");
|
||||
const year = card.querySelector("[data-year]");
|
||||
|
||||
initials.textContent = book.initials;
|
||||
title.textContent = book.title;
|
||||
author.textContent = book.author;
|
||||
year.textContent = book.year;
|
||||
|
||||
bookCardContainer.append(card);
|
||||
|
||||
return { initials: book.initials, title: book.title, author: book.author, year: book.year, element: card };
|
||||
});
|
||||
});
|
||||
|
||||
// Create a function to filter the books array based on the search input value.
|
||||
const filterBooks = (value: string) => {
|
||||
return books.filter((book) => {
|
||||
return book.author.toLowerCase().includes(value) ||
|
||||
book.title.toLowerCase().includes(value);
|
||||
});
|
||||
};
|
||||
|
||||
// Add a change event listener to the search input field.
|
||||
searchInput.addEventListener("input", (e: any) => {
|
||||
// Get the search input value.
|
||||
const value = e.target.value.toLowerCase();
|
||||
|
||||
// Filter the books array based on the search input value.
|
||||
const filteredBooks = filterBooks(value);
|
||||
|
||||
// Update the visibility of the book elements.
|
||||
books.forEach((book) => {
|
||||
book.element.classList.toggle("hidden", !filteredBooks.includes(book));
|
||||
});
|
||||
|
||||
// Update UI when no books are found
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("app-root", AppElement);
|
||||
6
part-2-javascript/src/app/book.interface.ts
Normal file
6
part-2-javascript/src/app/book.interface.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface Book {
|
||||
author: string;
|
||||
initials: string;
|
||||
title: string;
|
||||
year: number;
|
||||
}
|
||||
0
part-2-javascript/src/assets/.gitkeep
Normal file
0
part-2-javascript/src/assets/.gitkeep
Normal file
26
part-2-javascript/src/assets/books.json
Normal file
26
part-2-javascript/src/assets/books.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"author": "F. Scott Fitzgerald",
|
||||
"initials": "FS",
|
||||
"title": "The Great Gatsby",
|
||||
"year": 1925
|
||||
},
|
||||
{
|
||||
"author": "Harper Lee",
|
||||
"initials": "HL",
|
||||
"title": "To Kill a Mockingbird",
|
||||
"year": 1960
|
||||
},
|
||||
{
|
||||
"author": "Stephenie Meyer",
|
||||
"initials": "SM",
|
||||
"title": "Twilight",
|
||||
"year": 2005
|
||||
},
|
||||
{
|
||||
"author": "Stephen King",
|
||||
"initials": "S",
|
||||
"title": "If It Bleeds",
|
||||
"year": 2020
|
||||
}
|
||||
]
|
||||
3
part-2-javascript/src/environments/environment.prod.ts
Normal file
3
part-2-javascript/src/environments/environment.prod.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const environment = {
|
||||
production: true
|
||||
};
|
||||
6
part-2-javascript/src/environments/environment.ts
Normal file
6
part-2-javascript/src/environments/environment.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// When building for production, this file is replaced with `environment.prod.ts`.
|
||||
|
||||
export const environment = {
|
||||
production: false
|
||||
};
|
||||
19
part-2-javascript/src/index.html
Normal file
19
part-2-javascript/src/index.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-gray-100">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Part2Javascript</title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="https://www.payspace.com/wp-content/uploads/2018/03/PS-FAVICON-NEW-2017.png" sizes="32x32" />
|
||||
<link rel="icon" href="https://www.payspace.com/wp-content/uploads/2018/03/PS-FAVICON-NEW-2017.png" sizes="192x192" />
|
||||
<link rel="apple-touch-icon" href="https://www.payspace.com/wp-content/uploads/2018/03/PS-FAVICON-NEW-2017.png" />
|
||||
<meta name="msapplication-TileImage"
|
||||
content="https://www.payspace.com/wp-content/uploads/2018/03/PS-FAVICON-NEW-2017.png" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
1
part-2-javascript/src/main.ts
Normal file
1
part-2-javascript/src/main.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
import './app/app.element';
|
||||
1
part-2-javascript/src/styles.scss
Normal file
1
part-2-javascript/src/styles.scss
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
||||
0
part-2-javascript/src/test-setup.ts
Normal file
0
part-2-javascript/src/test-setup.ts
Normal file
9
part-2-javascript/tsconfig.app.json
Normal file
9
part-2-javascript/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist/out-tsc",
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
13
part-2-javascript/tsconfig.json
Normal file
13
part-2-javascript/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
part-2-javascript/tsconfig.spec.json
Normal file
15
part-2-javascript/tsconfig.spec.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
9
part-2-javascript/webpack.config.js
Normal file
9
part-2-javascript/webpack.config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
const { composePlugins, withNx } = require('@nx/webpack');
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(withNx(), (config) => {
|
||||
// Update the webpack config as needed here.
|
||||
// e.g. `config.plugins.push(new MyPlugin())`
|
||||
return config;
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue