Added responsive design classes, moved some things to common folder, added loading states

This commit is contained in:
cscough 2024-05-26 22:02:37 -04:00
parent f1b3d9e5be
commit 81e0e88eaa
27 changed files with 486 additions and 174 deletions

View File

@ -1,6 +1,5 @@
import { ReactEventHandler, useState } from "react";
export default ({ pages, onPageChange, currentPage }: any) => {
export default function Pagination({ pages, onPageChange, currentPage }: any) {
const [page, setPage] = useState(currentPage);
const handleClick = (e: any) => {
const { value } = e.target;
@ -62,4 +61,4 @@ export default ({ pages, onPageChange, currentPage }: any) => {
<QuickLink value={">>"} />
</>
);
};
}

View File

@ -1,6 +1,6 @@
import "@testing-library/jest-dom";
import {render, screen} from '@testing-library/react'
import MovieCard from '../src/app/Search/MovieCard.tsx'
import MovieCard from '../src/common/components/MovieCard.tsx'
const movie = {
Title: "Dune",
Year: "2000",

View File

@ -1,8 +1,19 @@
import "@testing-library/jest-dom";
import { queryOMDb } from "../src/api/omdb.ts";
test("queryOMDb returns movie data", async () => {
test("queryOMDb returns movie data successfully", async () => {
const data = await queryOMDb("s=back to");
expect(data).toHaveProperty("Response");
expect(data).toHaveProperty("Response", "True");
});
test("queryOMDb returns movie with error", async () => {
const data = await queryOMDb("f=dune");
expect(data).toHaveProperty("Response", "False");
});
// f=dune
// Warning: React does not recognize the `fetchPriority` prop on a DOM element
// The `punycode` module is deprecated. Please use a userland alternative instead.

View File

@ -10,18 +10,19 @@
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"framer-motion": "^11.2.6",
"next": "14.2.3",
"next": "^14.2.3",
"react": "^18",
"react-dom": "^18",
"ts-node-dev": "^2.0.0"
},
"devDependencies": {
"@babel/preset-typescript": "^7.24.6",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.19",
"eslint": "^8",
"eslint-config-next": "14.2.3",
@ -157,6 +158,18 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-annotate-as-pure": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz",
"integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==",
"dev": true,
"dependencies": {
"@babel/types": "^7.24.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz",
@ -191,6 +204,38 @@
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.6.tgz",
"integrity": "sha512-djsosdPJVZE6Vsw3kk7IPRWethP94WHGOhQTc67SNXE0ZzMhHgALw8iGmYS0TD1bbMM0VDROy43od7/hN6WYcA==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.24.6",
"@babel/helper-environment-visitor": "^7.24.6",
"@babel/helper-function-name": "^7.24.6",
"@babel/helper-member-expression-to-functions": "^7.24.6",
"@babel/helper-optimise-call-expression": "^7.24.6",
"@babel/helper-replace-supers": "^7.24.6",
"@babel/helper-skip-transparent-expression-wrappers": "^7.24.6",
"@babel/helper-split-export-declaration": "^7.24.6",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz",
@ -225,6 +270,18 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.6.tgz",
"integrity": "sha512-OTsCufZTxDUsv2/eDXanw/mUZHWOxSbEmC3pP8cgjcy5rgeVPWWMStnv274DV60JtHxTk0adT0QrCzC4M9NWGg==",
"dev": true,
"dependencies": {
"@babel/types": "^7.24.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz",
@ -255,6 +312,18 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-optimise-call-expression": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.6.tgz",
"integrity": "sha512-3SFDJRbx7KuPRl8XDUr8O7GAEB8iGyWPjLKJh/ywP/Iy9WOmEfMrsWbaZpvBu2HSYn4KQygIsz0O7m8y10ncMA==",
"dev": true,
"dependencies": {
"@babel/types": "^7.24.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz",
@ -264,6 +333,23 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-replace-supers": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.6.tgz",
"integrity": "sha512-mRhfPwDqDpba8o1F8ESxsEkJMQkUF8ZIWrAc0FtWhxnjfextxMWxr22RtFizxxSYLjVHDeMgVsRq8BBZR2ikJQ==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.24.6",
"@babel/helper-member-expression-to-functions": "^7.24.6",
"@babel/helper-optimise-call-expression": "^7.24.6"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-simple-access": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz",
@ -276,6 +362,18 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.6.tgz",
"integrity": "sha512-jhbbkK3IUKc4T43WadP96a27oYti9gEf1LdyGSP2rHGH77kwLwfhO7TgwnWvxxQVmke0ImmCSS47vcuxEMGD3Q==",
"dev": true,
"dependencies": {
"@babel/types": "^7.24.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz",
@ -593,6 +691,60 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-modules-commonjs": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.6.tgz",
"integrity": "sha512-JEV8l3MHdmmdb7S7Cmx6rbNEjRCgTQMZxllveHO0mx6uiclB0NflCawlQQ6+o5ZrwjUBYPzHm2XoK4wqGVUFuw==",
"dev": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.24.6",
"@babel/helper-plugin-utils": "^7.24.6",
"@babel/helper-simple-access": "^7.24.6"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-typescript": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.6.tgz",
"integrity": "sha512-H0i+hDLmaYYSt6KU9cZE0gb3Cbssa/oxWis7PX4ofQzbvsfix9Lbh8SRk7LCPDlLWJHUiFeHU0qRRpF/4Zv7mQ==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.24.6",
"@babel/helper-create-class-features-plugin": "^7.24.6",
"@babel/helper-plugin-utils": "^7.24.6",
"@babel/plugin-syntax-typescript": "^7.24.6"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/preset-typescript": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.6.tgz",
"integrity": "sha512-U10aHPDnokCFRXgyT/MaIRTivUu2K/mu0vJlwRS9LxJmJet+PFQNKpggPyFCUtC6zWSBPjvxjnpNkAn3Uw2m5w==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.24.6",
"@babel/helper-validator-option": "^7.24.6",
"@babel/plugin-syntax-jsx": "^7.24.6",
"@babel/plugin-transform-modules-commonjs": "^7.24.6",
"@babel/plugin-transform-typescript": "^7.24.6"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
@ -1930,14 +2082,12 @@
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
"dev": true
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
},
"node_modules/@types/react": {
"version": "18.3.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz",
"integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==",
"dev": true,
"version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@ -1947,7 +2097,6 @@
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
@ -9473,6 +9622,15 @@
"jsesc": "^2.5.1"
}
},
"@babel/helper-annotate-as-pure": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz",
"integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==",
"dev": true,
"requires": {
"@babel/types": "^7.24.6"
}
},
"@babel/helper-compilation-targets": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz",
@ -9503,6 +9661,31 @@
}
}
},
"@babel/helper-create-class-features-plugin": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.6.tgz",
"integrity": "sha512-djsosdPJVZE6Vsw3kk7IPRWethP94WHGOhQTc67SNXE0ZzMhHgALw8iGmYS0TD1bbMM0VDROy43od7/hN6WYcA==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.24.6",
"@babel/helper-environment-visitor": "^7.24.6",
"@babel/helper-function-name": "^7.24.6",
"@babel/helper-member-expression-to-functions": "^7.24.6",
"@babel/helper-optimise-call-expression": "^7.24.6",
"@babel/helper-replace-supers": "^7.24.6",
"@babel/helper-skip-transparent-expression-wrappers": "^7.24.6",
"@babel/helper-split-export-declaration": "^7.24.6",
"semver": "^6.3.1"
},
"dependencies": {
"semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true
}
}
},
"@babel/helper-environment-visitor": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz",
@ -9528,6 +9711,15 @@
"@babel/types": "^7.24.6"
}
},
"@babel/helper-member-expression-to-functions": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.6.tgz",
"integrity": "sha512-OTsCufZTxDUsv2/eDXanw/mUZHWOxSbEmC3pP8cgjcy5rgeVPWWMStnv274DV60JtHxTk0adT0QrCzC4M9NWGg==",
"dev": true,
"requires": {
"@babel/types": "^7.24.6"
}
},
"@babel/helper-module-imports": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz",
@ -9549,12 +9741,32 @@
"@babel/helper-validator-identifier": "^7.24.6"
}
},
"@babel/helper-optimise-call-expression": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.6.tgz",
"integrity": "sha512-3SFDJRbx7KuPRl8XDUr8O7GAEB8iGyWPjLKJh/ywP/Iy9WOmEfMrsWbaZpvBu2HSYn4KQygIsz0O7m8y10ncMA==",
"dev": true,
"requires": {
"@babel/types": "^7.24.6"
}
},
"@babel/helper-plugin-utils": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz",
"integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==",
"dev": true
},
"@babel/helper-replace-supers": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.6.tgz",
"integrity": "sha512-mRhfPwDqDpba8o1F8ESxsEkJMQkUF8ZIWrAc0FtWhxnjfextxMWxr22RtFizxxSYLjVHDeMgVsRq8BBZR2ikJQ==",
"dev": true,
"requires": {
"@babel/helper-environment-visitor": "^7.24.6",
"@babel/helper-member-expression-to-functions": "^7.24.6",
"@babel/helper-optimise-call-expression": "^7.24.6"
}
},
"@babel/helper-simple-access": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz",
@ -9564,6 +9776,15 @@
"@babel/types": "^7.24.6"
}
},
"@babel/helper-skip-transparent-expression-wrappers": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.6.tgz",
"integrity": "sha512-jhbbkK3IUKc4T43WadP96a27oYti9gEf1LdyGSP2rHGH77kwLwfhO7TgwnWvxxQVmke0ImmCSS47vcuxEMGD3Q==",
"dev": true,
"requires": {
"@babel/types": "^7.24.6"
}
},
"@babel/helper-split-export-declaration": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz",
@ -9793,6 +10014,42 @@
"@babel/helper-plugin-utils": "^7.24.6"
}
},
"@babel/plugin-transform-modules-commonjs": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.6.tgz",
"integrity": "sha512-JEV8l3MHdmmdb7S7Cmx6rbNEjRCgTQMZxllveHO0mx6uiclB0NflCawlQQ6+o5ZrwjUBYPzHm2XoK4wqGVUFuw==",
"dev": true,
"requires": {
"@babel/helper-module-transforms": "^7.24.6",
"@babel/helper-plugin-utils": "^7.24.6",
"@babel/helper-simple-access": "^7.24.6"
}
},
"@babel/plugin-transform-typescript": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.6.tgz",
"integrity": "sha512-H0i+hDLmaYYSt6KU9cZE0gb3Cbssa/oxWis7PX4ofQzbvsfix9Lbh8SRk7LCPDlLWJHUiFeHU0qRRpF/4Zv7mQ==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.24.6",
"@babel/helper-create-class-features-plugin": "^7.24.6",
"@babel/helper-plugin-utils": "^7.24.6",
"@babel/plugin-syntax-typescript": "^7.24.6"
}
},
"@babel/preset-typescript": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.6.tgz",
"integrity": "sha512-U10aHPDnokCFRXgyT/MaIRTivUu2K/mu0vJlwRS9LxJmJet+PFQNKpggPyFCUtC6zWSBPjvxjnpNkAn3Uw2m5w==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.24.6",
"@babel/helper-validator-option": "^7.24.6",
"@babel/plugin-syntax-jsx": "^7.24.6",
"@babel/plugin-transform-modules-commonjs": "^7.24.6",
"@babel/plugin-transform-typescript": "^7.24.6"
}
},
"@babel/runtime": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
@ -10815,14 +11072,12 @@
"@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
"dev": true
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
},
"@types/react": {
"version": "18.3.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz",
"integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==",
"dev": true,
"version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"requires": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@ -10832,7 +11087,6 @@
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"dev": true,
"requires": {
"@types/react": "*"
}

View File

@ -13,18 +13,19 @@
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"framer-motion": "^11.2.6",
"next": "14.2.3",
"next": "^14.2.3",
"react": "^18",
"react-dom": "^18",
"ts-node-dev": "^2.0.0"
},
"devDependencies": {
"@babel/preset-typescript": "^7.24.6",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.19",
"eslint": "^8",
"eslint-config-next": "14.2.3",

View File

@ -3,6 +3,8 @@ const url = `${process.env.OMDB_URL}?apikey=${process.env.API_KEY}`;
export const queryOMDb = async (queryParamString: string) => {
"use server";
try {
// await new Promise((resolve) => setTimeout(resolve, 2000));
const queryUrl = `${url}&${queryParamString}`;
const response = await fetch(queryUrl);
if (!response.ok) {

View File

@ -1,15 +1,12 @@
'use client'
"use client";
import { FavoriteContext } from "@/components/FavoriteContext";
import { FavoriteContext } from "@/common/components/FavoriteContext";
import MovieList from "@/common/components/MovieList";
import { useContext } from "react";
export default () => {
const {favorites} = useContext(FavoriteContext)
export default function FavoriteList() {
const { favorites } = useContext(FavoriteContext);
return (
<ul>
{favorites.map((f, i) => (
<li key={i}>{f.Title}</li>
))}
</ul>
<MovieList movies={favorites} />
);
};
}

View File

@ -1,6 +1,6 @@
import FavoriteList from "./favoriteList";
export default () => {
export default function Favorites() {
return (
<div>
<h1>Your Favorites!</h1>

View File

@ -0,0 +1,5 @@
import Loading from "@/common/components/Loading";
export default function loading() {
return <Loading />;
}

View File

@ -1,21 +1,22 @@
import { queryOMDb } from "@/api/omdb";
import { INextJsProps, stringifyQueryParams } from "../Search/page";
import ImgCard from "@/components/ImgCard";
import { INextJsProps } from "../Search/page";
import ImgCard from "@/common/components/ImgCard";
import { stringifyQueryParams } from "@/common/utils/searchParams";
export default async ({ searchParams }: INextJsProps) => {
export default async function Movie({ searchParams }: INextJsProps) {
const movie = await queryOMDb(stringifyQueryParams(searchParams!));
const { Poster, Ratings, ...rest } = movie;
const { Poster, Title, Ratings, ...details } = movie;
return (
<div className="rounded p-4 bg-gray-300 w-auto mx-4 flex-column">
<div className="rounded p-4 bg-gray-300 w-auto mx-4 flex-column h-xl">
<ImgCard
width={400}
height={580}
src={movie.Poster !== "N/A" ? movie.Poster : ""}
alt={`${movie.Title} Movie Poster`}
src={Poster !== "N/A" ? Poster : ""}
alt={`${details.Title} Movie Poster`}
>
<div className="mt-auto">
<h3 className="mb-2 text-lg font-semibold">{movie.Title}</h3>
{Object.entries(rest).map(([key, value]) => {
<h3 className="mb-2 text-lg font-semibold">{Title}</h3>
{Object.entries(details).map(([key, value]) => {
return (
<p key={key} className="mb-2 text-sm text-gray-700">
<strong>{key}: </strong>
@ -25,7 +26,7 @@ export default async ({ searchParams }: INextJsProps) => {
})}
<h3>Ratings:</h3>
<ul>
{movie.Ratings.map((r: { Source: string; Value: string }) => (
{Ratings.map((r: { Source: string; Value: string }) => (
<li key={r.Source}>
{r.Source} - {r.Value}
</li>
@ -35,4 +36,4 @@ export default async ({ searchParams }: INextJsProps) => {
</ImgCard>
</div>
);
};
}

View File

@ -1,40 +0,0 @@
import FavoriteBtn from "@/components/FavoriteBtn";
import ImgCard from "@/components/ImgCard";
import Link from "next/link";
export interface IMovieCardItem {
Title: string;
Year: string;
imdbID: string;
Type: string;
Poster: string;
}
interface IMovieCardProps {
movie: IMovieCardItem;
}
export default ({ movie }: IMovieCardProps) => {
return (
<ImgCard
classNames={`w-48`}
width={140}
height={160}
src={movie.Poster !== "N/A" ? movie.Poster : ""}
alt={`${movie.Title} Movie Poster`}
>
<div className="mt-auto">
<h3 className="mb-2 text-lg font-semibold">{movie.Title}</h3>
<p className="mb-2 text-sm text-gray-700">Circa: {movie.Year}</p>
</div>
<div className="flex gap-3">
<Link
href={`/Movie?i=${movie.imdbID}`}
className="block rounded-lg bg-purple-500 px-4 py-2 text-center font-semibold text-white hover:bg-purple-600"
>
Movie Details
</Link>
<FavoriteBtn movie={movie} />
</div>
</ImgCard>
);
};

View File

@ -1,19 +0,0 @@
import MovieCard, { IMovieCardItem } from "./MovieCard";
interface IMovieListProps {
movies: Array<IMovieCardItem>;
}
export default ({ movies }: IMovieListProps) => {
return (
<div className="container m-auto">
<div className={`p-4 flex justify-start flex-wrap gap-4`}>
{movies.map((m) => (
<div className="WrapItem" key={m.imdbID}>
<MovieCard movie={m} />
</div>
))}
</div>
</div>
);
};

View File

@ -1,20 +1,24 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export default () => {
export default function SearchBar() {
const searchMovies = async (formData: FormData) => {
"use server";
const movieTitle = formData.get("movieTitle");
// revalidatePath('/Search')
redirect(`/Search?s=${movieTitle}`);
};
return (
<form action={searchMovies} className={`flex gap-4 m-4 justify-center`}>
<form action={searchMovies} className={`md:flex gap-4 m-4 justify-center`}>
<div className="flex justify-center">
<input
className="rounded px-2"
className="rounded px-2 mb-2 md:mb-0"
placeholder="Search by Title"
name="movieTitle"
/>
<div className="flex gap-4">
</div>
<div className="flex gap-4 justify-center">
<button
type="submit"
className={`rounded px-2 bg-purple-500 hover:bg-purple-700`}
@ -30,4 +34,4 @@ export default () => {
</div>
</form>
);
};
}

View File

@ -0,0 +1,16 @@
import { queryOMDb } from "@/api/omdb";
import MovieList from "../../common/components/MovieList";
export default async function SearchPage({
queryParamString,
}: {
queryParamString: string;
}) {
const { Search } = await queryOMDb(queryParamString);
const movies = Search || [];
return (
<MovieList movies={movies} />
);
}

View File

@ -1,29 +1,22 @@
import { queryOMDb } from "@/api/omdb";
import MovieList from "./MovieList";
import { stringifyQueryParams } from "@/common/utils/searchParams";
import { Suspense } from "react";
import SearchPage from "./SearchPage";
import SearchBar from "./SearchBar";
import Loading from "@/common/components/Loading";
export interface INextJsProps {
searchParams?: object;
}
export const stringifyQueryParams = (searchParams: object) =>
Object.entries(searchParams)
.map(
([key, value], i, arr) =>
`${key}=${value}${i < arr.length - 1 ? "&" : ""}`
)
.toString();
export default async ({ searchParams }: INextJsProps) => {
export default async function Search({ searchParams }: INextJsProps) {
const queryParamString = stringifyQueryParams(searchParams!);
// console.log(queryParamString)
const { Search } = await queryOMDb(queryParamString);
const movies = Search || [];
return (
<div className={`mx-auto px-2`}>
<SearchBar />
<MovieList movies={movies} />
<Suspense key={queryParamString} fallback={<Loading />}>
<SearchPage queryParamString={queryParamString} />
</Suspense>
</div>
);
};
}

View File

@ -51,12 +51,12 @@
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--primary-glow: radial-gradient(rgba(210, 1, 255, 0.5), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
rgba(180, 1, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
@ -92,6 +92,7 @@ body {
body {
color: rgb(var(--foreground-rgb));
/* background: #2bff0110 */
background: linear-gradient(
to bottom,
transparent,

View File

@ -1,14 +1,14 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import NavBar from "@/components/navBar";
import { FavoriteProvider } from "@/components/FavoriteContext";
import NavBar from "@/common/components/NavBar";
import { FavoriteProvider } from "@/common/components/FavoriteContext";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Movie Search App",
description: "Powered by Nextjs and OMDb.",
};
export default function RootLayout({
@ -17,8 +17,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${inter.className} mb-4`}>
<html lang="en" className="h-full">
<body className={`${inter.className} mb-4 bg-black h-full`}>
<NavBar />
<FavoriteProvider>
{children}

View File

@ -0,0 +1 @@
const responsiveContainer = ''

View File

@ -0,0 +1,29 @@
"use client";
import { useContext } from "react";
import { FavoriteContext } from "./FavoriteContext";
export default function FavoriteBtn({ movie }: any) {
const { favorites, setFavorites } = useContext(FavoriteContext);
const filteredFavorites = favorites.filter((f) => f.imdbID != movie.imdbID);
const isFavorite =
favorites.filter((f) => f.imdbID == movie.imdbID).length == 1;
return (
<button
onClick={() => {
if (isFavorite) {
setFavorites([...filteredFavorites]);
} else {
setFavorites([...favorites, movie]);
}
}}
className={`block text-yellow-500 rounded my-auto h-8 w-8
${
isFavorite ? " bg-black" : "border-2 border-gray-300 bg-white"
}`}
>
</button>
);
}

View File

@ -1,6 +1,7 @@
'use client'
import { IMovieCardItem } from "@/app/Search/MovieCard";
import { PropsWithChildren, createContext, useState } from "react";
import { IMovieCardItem } from "./MovieCard";
interface IFavoriteContext {
favorites: Array<IMovieCardItem>

View File

@ -7,19 +7,18 @@ interface ICardProps extends PropsWithChildren {
alt: string;
classNames?: string;
}
export default ({
export default function ImgCard({
classNames,
width,
height,
src,
alt,
children,
}: ICardProps) => {
}: ICardProps) {
return (
<div
className={`
shadow-md
shadow
shadow-lg
shadow-purple-500
rounded

View File

@ -0,0 +1,9 @@
export default function Loading() {
return (
<div className="h-svh flex flex-col justify-center">
<h1 className="my-auto text-center">
<strong>Loading...</strong>
</h1>
</div>
);
}

View File

@ -0,0 +1,41 @@
import FavoriteBtn from "@/common/components/FavoriteBtn";
import ImgCard from "@/common/components/ImgCard";
import Link from "next/link";
export interface IMovieCardItem {
Title: string;
Year: string;
imdbID: string;
Type: string;
Poster: string;
}
interface IMovieCardProps {
movie: IMovieCardItem;
}
export default function MovieCard({ movie }: IMovieCardProps) {
return (
<ImgCard
classNames={`w-full h-full flex flex-col justify-between`}
width={140}
height={160}
src={movie.Poster !== "N/A" ? movie.Poster : ""}
alt={`${movie.Title} Movie Poster`}
>
<div className="flex flex-col gap-4">
<h3 className=" text-lg font-semibold">{movie.Title}</h3>
<p className=" text-sm text-gray-700">Circa: {movie.Year}</p>
<div className="flex gap-3 justify-around">
<Link
href={`/Movie?i=${movie.imdbID}`}
className="block rounded-lg bg-purple-500 px-4 py-2 text-center font-semibold text-white hover:bg-purple-600"
>
Movie Details
</Link>
<FavoriteBtn movie={movie} />
</div>
</div>
</ImgCard>
);
}

View File

@ -0,0 +1,25 @@
import MovieCard, { IMovieCardItem } from "./MovieCard";
export interface IMovieListProps {
movies: Array<IMovieCardItem>;
}
export default function MovieList({ movies }: IMovieListProps) {
return (
<div className="container m-auto">
<div
className={`p-1 flex justify-around flex-wrap gap-4 sm:justify-center `}
>
{movies.length > 0 ? (
movies.map((m) => (
<div className="WrapItem w-full mx-5 sm:w-64 mx-1" key={m.imdbID}>
<MovieCard movie={m} />
</div>
))
) : (
<h1>Nothing to show</h1>
)}
</div>
</div>
);
}

View File

@ -1,8 +1,8 @@
import Link from "next/link";
export default () => {
export default function NavBar() {
return (
<nav className="px-2 flex gap-4">
<nav className="px-2 flex gap-4 shadow-md shadow-purple-500 mt-2 mb-4">
<Link href="/">
<strong>Home</strong>
</Link>
@ -10,4 +10,4 @@ export default () => {
<Link href="/Favorites">Favorites</Link>
</nav>
);
};
}

View File

@ -0,0 +1,7 @@
export const stringifyQueryParams = (searchParams: object) =>
Object.entries(searchParams)
.map(
([key, value], i, arr) =>
`${key}=${value}${i < arr.length - 1 ? "&" : ""}`
)
.toString();

View File

@ -1,25 +0,0 @@
"use client";
import { useContext } from "react";
import { FavoriteContext } from "./FavoriteContext";
export default ({ movie }: any) => {
const { favorites, setFavorites } = useContext(FavoriteContext);
const filteredFavorites = favorites.filter(f => f.imdbID != movie.imdbID)
const isFavorite = favorites.filter(f => f.imdbID == movie.imdbID).length == 1
return (
<button
onClick={() => {
if (isFavorite) {
setFavorites([...filteredFavorites])
} else {
setFavorites([...favorites, movie]);
}
}}
className={`block text-yellow-500 rounded my-auto w-6 ${isFavorite ? 'bg-black' : 'bg-white'}`}
>
</button>
);
};