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

View File

@ -1,6 +1,6 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import {render, screen} from '@testing-library/react' 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 = { const movie = {
Title: "Dune", Title: "Dune",
Year: "2000", Year: "2000",

View File

@ -1,8 +1,19 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { queryOMDb } from "../src/api/omdb.ts"; 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"); 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": { "dependencies": {
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"framer-motion": "^11.2.6", "framer-motion": "^11.2.6",
"next": "14.2.3", "next": "^14.2.3",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"ts-node-dev": "^2.0.0" "ts-node-dev": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-typescript": "^7.24.6",
"@testing-library/jest-dom": "^6.4.5", "@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.2.3", "eslint-config-next": "14.2.3",
@ -157,6 +158,18 @@
"node": ">=6.9.0" "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": { "node_modules/@babel/helper-compilation-targets": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz",
@ -191,6 +204,38 @@
"semver": "bin/semver.js" "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": { "node_modules/@babel/helper-environment-visitor": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", "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": ">=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": { "node_modules/@babel/helper-module-imports": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", "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" "@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": { "node_modules/@babel/helper-plugin-utils": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", "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": ">=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": { "node_modules/@babel/helper-simple-access": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", "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": ">=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": { "node_modules/@babel/helper-split-export-declaration": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", "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" "@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": { "node_modules/@babel/runtime": {
"version": "7.24.5", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
@ -1930,14 +2082,12 @@
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.12", "version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
"dev": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.2", "version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"dev": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -1947,7 +2097,6 @@
"version": "18.3.0", "version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"dev": true,
"dependencies": { "dependencies": {
"@types/react": "*" "@types/react": "*"
} }
@ -9473,6 +9622,15 @@
"jsesc": "^2.5.1" "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": { "@babel/helper-compilation-targets": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", "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": { "@babel/helper-environment-visitor": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", "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/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": { "@babel/helper-module-imports": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", "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-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": { "@babel/helper-plugin-utils": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz",
"integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==", "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==",
"dev": true "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": { "@babel/helper-simple-access": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", "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/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": { "@babel/helper-split-export-declaration": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", "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/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": { "@babel/runtime": {
"version": "7.24.5", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
@ -10815,14 +11072,12 @@
"@types/prop-types": { "@types/prop-types": {
"version": "15.7.12", "version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
"dev": true
}, },
"@types/react": { "@types/react": {
"version": "18.3.2", "version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"dev": true,
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -10832,7 +11087,6 @@
"version": "18.3.0", "version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"dev": true,
"requires": { "requires": {
"@types/react": "*" "@types/react": "*"
} }

View File

@ -13,18 +13,19 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"framer-motion": "^11.2.6", "framer-motion": "^11.2.6",
"next": "14.2.3", "next": "^14.2.3",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"ts-node-dev": "^2.0.0" "ts-node-dev": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-typescript": "^7.24.6",
"@testing-library/jest-dom": "^6.4.5", "@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.2.3", "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) => { export const queryOMDb = async (queryParamString: string) => {
"use server"; "use server";
try { try {
// await new Promise((resolve) => setTimeout(resolve, 2000));
const queryUrl = `${url}&${queryParamString}`; const queryUrl = `${url}&${queryParamString}`;
const response = await fetch(queryUrl); const response = await fetch(queryUrl);
if (!response.ok) { 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"; import { useContext } from "react";
export default () => { export default function FavoriteList() {
const {favorites} = useContext(FavoriteContext) const { favorites } = useContext(FavoriteContext);
return ( return (
<ul> <MovieList movies={favorites} />
{favorites.map((f, i) => (
<li key={i}>{f.Title}</li>
))}
</ul>
); );
}; }

View File

@ -1,6 +1,6 @@
import FavoriteList from "./favoriteList"; import FavoriteList from "./favoriteList";
export default () => { export default function Favorites() {
return ( return (
<div> <div>
<h1>Your Favorites!</h1> <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 { queryOMDb } from "@/api/omdb";
import { INextJsProps, stringifyQueryParams } from "../Search/page"; import { INextJsProps } from "../Search/page";
import ImgCard from "@/components/ImgCard"; 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 movie = await queryOMDb(stringifyQueryParams(searchParams!));
const { Poster, Ratings, ...rest } = movie; const { Poster, Title, Ratings, ...details } = movie;
return ( 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 <ImgCard
width={400} width={400}
height={580} height={580}
src={movie.Poster !== "N/A" ? movie.Poster : ""} src={Poster !== "N/A" ? Poster : ""}
alt={`${movie.Title} Movie Poster`} alt={`${details.Title} Movie Poster`}
> >
<div className="mt-auto"> <div className="mt-auto">
<h3 className="mb-2 text-lg font-semibold">{movie.Title}</h3> <h3 className="mb-2 text-lg font-semibold">{Title}</h3>
{Object.entries(rest).map(([key, value]) => { {Object.entries(details).map(([key, value]) => {
return ( return (
<p key={key} className="mb-2 text-sm text-gray-700"> <p key={key} className="mb-2 text-sm text-gray-700">
<strong>{key}: </strong> <strong>{key}: </strong>
@ -25,7 +26,7 @@ export default async ({ searchParams }: INextJsProps) => {
})} })}
<h3>Ratings:</h3> <h3>Ratings:</h3>
<ul> <ul>
{movie.Ratings.map((r: { Source: string; Value: string }) => ( {Ratings.map((r: { Source: string; Value: string }) => (
<li key={r.Source}> <li key={r.Source}>
{r.Source} - {r.Value} {r.Source} - {r.Value}
</li> </li>
@ -35,4 +36,4 @@ export default async ({ searchParams }: INextJsProps) => {
</ImgCard> </ImgCard>
</div> </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"; import { redirect } from "next/navigation";
export default () => { export default function SearchBar() {
const searchMovies = async (formData: FormData) => { const searchMovies = async (formData: FormData) => {
"use server"; "use server";
const movieTitle = formData.get("movieTitle"); const movieTitle = formData.get("movieTitle");
// revalidatePath('/Search')
redirect(`/Search?s=${movieTitle}`); redirect(`/Search?s=${movieTitle}`);
}; };
return ( return (
<form action={searchMovies} className={`flex gap-4 m-4 justify-center`}> <form action={searchMovies} className={`md:flex gap-4 m-4 justify-center`}>
<input <div className="flex justify-center">
className="rounded px-2" <input
placeholder="Search by Title" className="rounded px-2 mb-2 md:mb-0"
name="movieTitle" placeholder="Search by Title"
/> name="movieTitle"
<div className="flex gap-4"> />
</div>
<div className="flex gap-4 justify-center">
<button <button
type="submit" type="submit"
className={`rounded px-2 bg-purple-500 hover:bg-purple-700`} className={`rounded px-2 bg-purple-500 hover:bg-purple-700`}
@ -30,4 +34,4 @@ export default () => {
</div> </div>
</form> </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 { stringifyQueryParams } from "@/common/utils/searchParams";
import MovieList from "./MovieList"; import { Suspense } from "react";
import SearchPage from "./SearchPage";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import Loading from "@/common/components/Loading";
export interface INextJsProps { export interface INextJsProps {
searchParams?: object; searchParams?: object;
} }
export const stringifyQueryParams = (searchParams: object) => export default async function Search({ searchParams }: INextJsProps) {
Object.entries(searchParams)
.map(
([key, value], i, arr) =>
`${key}=${value}${i < arr.length - 1 ? "&" : ""}`
)
.toString();
export default async ({ searchParams }: INextJsProps) => {
const queryParamString = stringifyQueryParams(searchParams!); const queryParamString = stringifyQueryParams(searchParams!);
// console.log(queryParamString)
const { Search } = await queryOMDb(queryParamString);
const movies = Search || [];
return ( return (
<div className={`mx-auto px-2`}> <div className={`mx-auto px-2`}>
<SearchBar /> <SearchBar />
<MovieList movies={movies} /> <Suspense key={queryParamString} fallback={<Loading />}>
<SearchPage queryParamString={queryParamString} />
</Suspense>
</div> </div>
); );
}; }

View File

@ -51,12 +51,12 @@
--background-start-rgb: 0, 0, 0; --background-start-rgb: 0, 0, 0;
--background-end-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( --secondary-glow: linear-gradient(
to bottom right, to bottom right,
rgba(1, 65, 255, 0), rgba(1, 65, 255, 0),
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; --tile-start-rgb: 2, 13, 46;
@ -92,6 +92,7 @@ body {
body { body {
color: rgb(var(--foreground-rgb)); color: rgb(var(--foreground-rgb));
/* background: #2bff0110 */
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
transparent, transparent,

View File

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

View File

@ -7,19 +7,18 @@ interface ICardProps extends PropsWithChildren {
alt: string; alt: string;
classNames?: string; classNames?: string;
} }
export default ({ export default function ImgCard({
classNames, classNames,
width, width,
height, height,
src, src,
alt, alt,
children, children,
}: ICardProps) => { }: ICardProps) {
return ( return (
<div <div
className={` className={`
shadow-md shadow-lg
shadow
shadow-purple-500 shadow-purple-500
rounded 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"; import Link from "next/link";
export default () => { export default function NavBar() {
return ( 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="/"> <Link href="/">
<strong>Home</strong> <strong>Home</strong>
</Link> </Link>
@ -10,4 +10,4 @@ export default () => {
<Link href="/Favorites">Favorites</Link> <Link href="/Favorites">Favorites</Link>
</nav> </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>
);
};