Compare commits
No commits in common. "504e335da71dc441756cf6ed1270c3747da19314" and "91fe3e6e5b2174b00c89bc1e7d0d1e345d0355e0" have entirely different histories.
504e335da7
...
91fe3e6e5b
24
.gitignore
vendored
24
.gitignore
vendored
|
@ -1,24 +0,0 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
|
@ -1,8 +0,0 @@
|
||||||
# React + Vite
|
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
|
@ -1,38 +0,0 @@
|
||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import react from 'eslint-plugin-react'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{ ignores: ['dist'] },
|
|
||||||
{
|
|
||||||
files: ['**/*.{js,jsx}'],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
ecmaFeatures: { jsx: true },
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
settings: { react: { version: '18.3' } },
|
|
||||||
plugins: {
|
|
||||||
react,
|
|
||||||
'react-hooks': reactHooks,
|
|
||||||
'react-refresh': reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...js.configs.recommended.rules,
|
|
||||||
...react.configs.recommended.rules,
|
|
||||||
...react.configs['jsx-runtime'].rules,
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
'react/jsx-no-target-blank': 'off',
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
13
index.html
13
index.html
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Star Wars | Assignment 4</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
5740
package-lock.json
generated
5740
package-lock.json
generated
File diff suppressed because it is too large
Load diff
36
package.json
36
package.json
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
"name": "star-wars",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.7.9",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"react-intersection-observer": "^9.14.1",
|
|
||||||
"react-loading-skeleton": "^3.5.0",
|
|
||||||
"react-router": "^6.28.1",
|
|
||||||
"react-router-dom": "^6.28.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^9.17.0",
|
|
||||||
"@types/react": "^18.3.18",
|
|
||||||
"@types/react-dom": "^18.3.5",
|
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"eslint": "^9.17.0",
|
|
||||||
"eslint-plugin-react": "^7.37.2",
|
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
|
||||||
"globals": "^15.14.0",
|
|
||||||
"postcss": "^8.4.49",
|
|
||||||
"tailwindcss": "^3.4.17",
|
|
||||||
"vite": "^6.0.5"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.1 KiB |
205
src/App.css
205
src/App.css
|
@ -1,205 +0,0 @@
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap");
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #000;
|
|
||||||
font-family: "Poppins", sans-serif;
|
|
||||||
color: #fff;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header img {
|
|
||||||
width: 150px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: auto;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-card {
|
|
||||||
background-color: #111;
|
|
||||||
border: 2px solid #ffcc00;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 250px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
transition: transform 0.3s, box-shadow 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-card:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 5px 15px rgba(255, 204, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-details p {
|
|
||||||
margin: 10px 0;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.character-details p strong {
|
|
||||||
color: #ffcc00;
|
|
||||||
text-transform: capitalize;
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.changePage {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background-color: #ffcc00;
|
|
||||||
color: #000;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 30px 10px;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background-color: #ffb103;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.character-card {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
width: 100px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search Functionality */
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
position: relative;
|
|
||||||
margin: 50px auto;
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
outline: none;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: #333;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-top: none;
|
|
||||||
height: auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-item {
|
|
||||||
padding: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-item:hover {
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.header-container {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header img {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SKELETON */
|
|
||||||
.skeleton-character-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 20px;
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-character-card {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
width: 250px;
|
|
||||||
height: 225px;
|
|
||||||
margin: 20px 0px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-character-details p {
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-line {
|
|
||||||
text-align: center;
|
|
||||||
background: linear-gradient(to right, #666 8%, #999 18%, #666 33%);
|
|
||||||
background-size: 1000px 100%;
|
|
||||||
animation: shimmer 3s infinite;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 120px;
|
|
||||||
height: 16px;
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: -1000px;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 1000px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.character-card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
30
src/App.jsx
30
src/App.jsx
|
@ -1,30 +0,0 @@
|
||||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
|
||||||
import "./App.css";
|
|
||||||
import Home from "./pages/Home";
|
|
||||||
import Error from "./components/Error";
|
|
||||||
import CharacterDetails from "./components/characterDetail/CharacterDetails";
|
|
||||||
function App() {
|
|
||||||
const router = createBrowserRouter([
|
|
||||||
{
|
|
||||||
errorElement: <Error />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <Home />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/characterDetails/:id",
|
|
||||||
element: <CharacterDetails />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,37 +0,0 @@
|
||||||
import "../App.css";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
const CharacterCard = (props) => {
|
|
||||||
return (
|
|
||||||
<ul className="character-container">
|
|
||||||
{props.data.map((character, index) => {
|
|
||||||
const id = character.url.split("/").filter(Boolean).pop();
|
|
||||||
return (
|
|
||||||
<li className="character-card" key={index}>
|
|
||||||
<Link to={`/characterDetails/${id}`}>
|
|
||||||
<div className="character-details">
|
|
||||||
<p>
|
|
||||||
<strong> Name </strong> {character.name}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong> Gender </strong> {character.gender}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong> Height </strong> {character.height} cm
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong> Hair Color </strong> {character.hair_color}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong> Birth Year </strong> {character.birth_year}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CharacterCard;
|
|
|
@ -1,16 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Link, useRouteError } from "react-router-dom";
|
|
||||||
const Error = () => {
|
|
||||||
const error = useRouteError();
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>OOPS! An error occured. </h1>
|
|
||||||
<p>{error.data} </p>
|
|
||||||
<Link to="/">
|
|
||||||
<button> Go Home </button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Error;
|
|
|
@ -1,26 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import "../App.css";
|
|
||||||
const Film = () => {
|
|
||||||
const [data, setData] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
axios.get(`https://swapi.py4e.com/api/films/`).then((res) => {
|
|
||||||
setData(res.data.results);
|
|
||||||
console.log(res.data.results);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<ul>
|
|
||||||
{data.map((elem, index) => {
|
|
||||||
return (
|
|
||||||
<li id="movieStyle" key={index}>
|
|
||||||
<p>Movie Title : {elem.title}</p>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Film;
|
|
|
@ -1,47 +0,0 @@
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
|
|
||||||
|
|
||||||
.character-detail-page {
|
|
||||||
background-color: #000;
|
|
||||||
font-family: "Poppins", sans-serif;
|
|
||||||
color: #ffffff;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
max-width: 700px;
|
|
||||||
margin: 40px auto;
|
|
||||||
box-shadow: 0px 5px 10px rgba(255, 255, 255, 0.3);
|
|
||||||
border: 2px solid #ffd700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-detail-page p {
|
|
||||||
margin: 12px 0;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-detail-page p strong {
|
|
||||||
color: #ffd700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-detail-page:hover {
|
|
||||||
box-shadow: 0 5px 15px 2px rgba(255, 204, 0, 0.9);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-detail-page p:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-detail-page p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-detail-page h2 {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-family: "Poppins", serif;
|
|
||||||
text-align: center;
|
|
||||||
color: #ffd700;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useParams } from "react-router";
|
|
||||||
import Film from "../Film";
|
|
||||||
import "../characterDetail/CharacterDetails.css";
|
|
||||||
const CharacterDetails = () => {
|
|
||||||
const { id } = useParams();
|
|
||||||
console.log(id);
|
|
||||||
const [character, setCharacter] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
axios
|
|
||||||
.get(`https://swapi.py4e.com/api/people/${id}/`)
|
|
||||||
.then((res) => {
|
|
||||||
setCharacter(res.data);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <p> loading...</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!character) {
|
|
||||||
return <p> Character Details not found!</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="character-detail-page">
|
|
||||||
<h2>Character Info </h2>
|
|
||||||
<p>
|
|
||||||
<strong> Name </strong> {character.name}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Gender </strong>
|
|
||||||
{character.gender}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Height </strong> {character.height} cm
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Hair Color </strong> {character.hair_color}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Skin Color </strong> {character.skin_color}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Eye Color </strong> {character.eye_color}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Birth Year </strong> {character.birth_year}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong> Films </strong> <Film />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CharacterDetails;
|
|
|
@ -1,3 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
10
src/main.jsx
10
src/main.jsx
|
@ -1,10 +0,0 @@
|
||||||
import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import './index.css'
|
|
||||||
import App from './App.jsx'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>,
|
|
||||||
)
|
|
|
@ -1,147 +0,0 @@
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import axios from "axios";
|
|
||||||
import { useInView } from "react-intersection-observer";
|
|
||||||
import Skeleton from "react-loading-skeleton";
|
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
|
||||||
import CharacterCard from "../components/CharacterCard";
|
|
||||||
|
|
||||||
const Home = () => {
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [data, setData] = useState([]);
|
|
||||||
const [filteredData, setFilteredData] = useState([]);
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [showSkeleton, setShowSkeleton] = useState(false);
|
|
||||||
const [suggestions, setSuggestions] = useState([]);
|
|
||||||
const [hasMore, setHasMore] = useState(true);
|
|
||||||
|
|
||||||
const { ref, inView } = useInView({
|
|
||||||
threshold: 0.1,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasMore) return;
|
|
||||||
|
|
||||||
setShowSkeleton(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
axios
|
|
||||||
.get(`https://swapi.py4e.com/api/people/?page=${page}`)
|
|
||||||
.then((res) => {
|
|
||||||
const results = res.data.results;
|
|
||||||
if (results.length > 0) {
|
|
||||||
setData((prevData) => [...prevData, ...results]);
|
|
||||||
setFilteredData((prevData) => [...prevData, ...results]);
|
|
||||||
} else {
|
|
||||||
setHasMore(false);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
setShowSkeleton(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setShowSkeleton(false);
|
|
||||||
setHasMore(false);
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}, [page]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inView && hasMore && !loading) {
|
|
||||||
setPage((prevPage) => prevPage + 1);
|
|
||||||
}
|
|
||||||
}, [inView, hasMore, loading]);
|
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
|
||||||
const query = e.target.value.toLowerCase();
|
|
||||||
setSearchQuery(query);
|
|
||||||
|
|
||||||
if (query === "") {
|
|
||||||
setFilteredData(data);
|
|
||||||
setSuggestions([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filtered = data.filter((character) =>
|
|
||||||
character.name.toLowerCase().includes(query)
|
|
||||||
);
|
|
||||||
setFilteredData(filtered);
|
|
||||||
|
|
||||||
const autoSuggestions = data
|
|
||||||
.map((character) => character.name)
|
|
||||||
.filter((name) => name.toLowerCase().includes(query))
|
|
||||||
.slice(0, 5);
|
|
||||||
setSuggestions(autoSuggestions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSuggestionClick = (suggestion) => {
|
|
||||||
setSearchQuery(suggestion);
|
|
||||||
setFilteredData(
|
|
||||||
data.filter(
|
|
||||||
(character) => character.name.toLowerCase() === suggestion.toLowerCase()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setSuggestions([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="main">
|
|
||||||
<div className="header-container">
|
|
||||||
<div className="header">
|
|
||||||
{loading ? (
|
|
||||||
<Skeleton width={120} height={60} />
|
|
||||||
) : (
|
|
||||||
<img src="/images/logo.png" alt="Logo" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="search-container">
|
|
||||||
{loading ? (
|
|
||||||
<Skeleton width={280} height={52} />
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search by name..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={handleSearch}
|
|
||||||
className="search-input"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{suggestions.length > 0 && (
|
|
||||||
<ul className="suggestions">
|
|
||||||
{suggestions.map((suggestion, index) => (
|
|
||||||
<li
|
|
||||||
key={index}
|
|
||||||
className="suggestion-item"
|
|
||||||
onClick={() => handleSuggestionClick(suggestion)}
|
|
||||||
>
|
|
||||||
{suggestion}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CharacterCard data={filteredData} />
|
|
||||||
{showSkeleton && hasMore && (
|
|
||||||
<ul className="skeleton-character-container">
|
|
||||||
{[...Array.from({ length: 5 })].map((_, index) => (
|
|
||||||
<li className="skeleton-character-card skeleton" key={index}>
|
|
||||||
<div className="skeleton-character-details">
|
|
||||||
<div className="skeleton-line skeleton-title"></div>
|
|
||||||
<div className="skeleton-line"></div>
|
|
||||||
<div className="skeleton-line"></div>
|
|
||||||
<div className="skeleton-line"></div>
|
|
||||||
<div className="skeleton-line"></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasMore && <div ref={ref}></div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Home;
|
|
|
@ -1,12 +0,0 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react-swc'
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
})
|
|
Loading…
Reference in a new issue