dev #1
BIN
public/215697_small.mp4
Normal file
BIN
public/215697_small.mp4
Normal file
Binary file not shown.
167
src/App.css
167
src/App.css
|
@ -14,10 +14,17 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.header img {
|
.header img {
|
||||||
width: 200px;
|
width: 150px;
|
||||||
height: 200px;
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 1200px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
animation: fadeIn 2s ease-in-out;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-container {
|
.character-container {
|
||||||
|
@ -43,19 +50,14 @@ body {
|
||||||
box-shadow: 0 5px 15px rgba(255, 204, 0, 0.5);
|
box-shadow: 0 5px 15px rgba(255, 204, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-image {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-details p {
|
.character-details p {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
color: #ffcc00;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.character-details p strong {
|
||||||
.changePage {
|
color: #ffcc00;
|
||||||
margin-top: 20px;
|
text-transform: capitalize;
|
||||||
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
@ -66,7 +68,7 @@ body {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0 10px;
|
margin: 30px 10px;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,33 +76,6 @@ body {
|
||||||
background-color: #ffb103;
|
background-color: #ffb103;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #ffcc00;
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.character-card {
|
.character-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -111,3 +86,113 @@ body {
|
||||||
font-size: 14px;
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
16
src/App.jsx
16
src/App.jsx
|
@ -1,33 +1,19 @@
|
||||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import About from "./pages/About";
|
|
||||||
import Contact from "./pages/Contact";
|
|
||||||
import Layout from "./components/layout/Layout";
|
|
||||||
import Error from "./components/Error";
|
import Error from "./components/Error";
|
||||||
import CharacterDetails from "./components/CharacterDetails";
|
import CharacterDetails from "./components/characterDetail/CharacterDetails";
|
||||||
function App() {
|
function App() {
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
|
||||||
element: <Layout />,
|
|
||||||
errorElement: <Error />,
|
errorElement: <Error />,
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
path: "/about",
|
|
||||||
element: <About />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/contact",
|
|
||||||
element: <Contact />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <Home />,
|
element: <Home />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/characterDetails/:id",
|
path: "/characterDetails/:id",
|
||||||
|
|
||||||
element: <CharacterDetails />,
|
element: <CharacterDetails />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import "../App.css";
|
import "../App.css";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
@ -6,17 +6,26 @@ const CharacterCard = (props) => {
|
||||||
return (
|
return (
|
||||||
<ul className="character-container">
|
<ul className="character-container">
|
||||||
{props.data.map((character, index) => {
|
{props.data.map((character, index) => {
|
||||||
// Extract ID from character's URL
|
|
||||||
const id = character.url.split("/").filter(Boolean).pop();
|
const id = character.url.split("/").filter(Boolean).pop();
|
||||||
return (
|
return (
|
||||||
<li className="character-card" key={index}>
|
<li className="character-card" key={index}>
|
||||||
<Link to={`/characterDetails/${id}`}>
|
<Link to={`/characterDetails/${id}`}>
|
||||||
<div className="character-details">
|
<div className="character-details">
|
||||||
<p>Name: {character.name}</p>
|
<p>
|
||||||
<p>Gender: {character.gender}</p>
|
<strong> Name </strong> {character.name}
|
||||||
<p>Height: {character.height} cm</p>
|
</p>
|
||||||
<p>Hair Color: {character.hair_color}</p>
|
<p>
|
||||||
<p>Birth Year: {character.birth_year}</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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const Footer = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
footer
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Footer
|
|
|
@ -1,7 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const Navbar = () => {
|
|
||||||
return <div>navbar</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Navbar;
|
|
47
src/components/characterDetail/CharacterDetails.css
Normal file
47
src/components/characterDetail/CharacterDetails.css
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
@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,6 +1,8 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
|
import Film from "../Film";
|
||||||
|
import "../characterDetail/CharacterDetails.css";
|
||||||
const CharacterDetails = () => {
|
const CharacterDetails = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
console.log(id);
|
console.log(id);
|
||||||
|
@ -30,29 +32,32 @@ const CharacterDetails = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="character-detail-page">
|
<div className="character-detail-page">
|
||||||
<p>{character.name}</p>
|
<h2>Character Info </h2>
|
||||||
<p>
|
<p>
|
||||||
<strong>Gender: </strong>
|
<strong> Name </strong> {character.name}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Gender </strong>
|
||||||
{character.gender}
|
{character.gender}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Home World: </strong>
|
<strong>Height </strong> {character.height} cm
|
||||||
{character.homeworld}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Species: </strong> {character.species}
|
<strong>Hair Color </strong> {character.hair_color}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Films: </strong> {character.films}
|
<strong>Skin Color </strong> {character.skin_color}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Height: </strong> {character.height} cm
|
<strong>Eye Color </strong> {character.eye_color}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Hair Color: </strong> {character.hair_color}
|
<strong>Birth Year </strong> {character.birth_year}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<strong>Birth Year: </strong> {character.birth_year}
|
<strong> Films </strong> <Film />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
|
@ -1,15 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { Outlet } from 'react-router'
|
|
||||||
import Navbar from '../Navbar'
|
|
||||||
import Footer from '../Footer'
|
|
||||||
const Layout = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Navbar />
|
|
||||||
<Outlet />
|
|
||||||
<Footer />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Layout;
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const About = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
about page
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default About
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const Contact = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
contact page
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Contact
|
|
|
@ -5,7 +5,10 @@ import CharacterCard from "../components/CharacterCard";
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [filteredData, setFilteredData] = useState([]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [suggestions, setSuggestions] = useState([]);
|
||||||
|
|
||||||
const increment = () => {
|
const increment = () => {
|
||||||
setPage(page + 1);
|
setPage(page + 1);
|
||||||
|
@ -16,21 +19,108 @@ const Home = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
|
||||||
axios.get(`https://swapi.py4e.com/api/people/?page=${page}`).then((res) => {
|
axios.get(`https://swapi.py4e.com/api/people/?page=${page}`).then((res) => {
|
||||||
setData(res.data.results);
|
setData(res.data.results);
|
||||||
setLoading(false);
|
setFilteredData(res.data.results);
|
||||||
});
|
});
|
||||||
}, [page]);
|
}, [page]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
const query = e.target.value.toLowerCase();
|
||||||
|
setSearchQuery(query);
|
||||||
|
|
||||||
|
if (query === "") {
|
||||||
|
setFilteredData(data);
|
||||||
|
setSuggestions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FILTERING DATA TO LOWERCASE
|
||||||
|
const filtered = data.filter((character) =>
|
||||||
|
character.name.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
setFilteredData(filtered);
|
||||||
|
|
||||||
|
// AUTOCOMPLETE SUGGESTION
|
||||||
|
const autoSuggestions = data
|
||||||
|
.map((character) => character.name)
|
||||||
|
.filter((name) => name.toLowerCase().includes(query))
|
||||||
|
.slice(0, 5);
|
||||||
|
setSuggestions(autoSuggestions);
|
||||||
|
};
|
||||||
|
|
||||||
|
// HANDLING SUGGESTION ONCLICK EVENT
|
||||||
|
const handleSuggestionClick = (suggestion) => {
|
||||||
|
setSearchQuery(suggestion);
|
||||||
|
setFilteredData(
|
||||||
|
data.filter(
|
||||||
|
(character) => character.name.toLowerCase() === suggestion.toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setSuggestions([]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="main">
|
<div className="main">
|
||||||
<div className="header">
|
<div className="header-container">
|
||||||
<img src="/images/logo.png" alt="Logo" />
|
<div className="header">
|
||||||
|
<img src="/images/logo.png" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="search-container">
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
{loading ? <p className="loading">Loading...</p> : <CharacterCard data={data} />}
|
{loading ? (
|
||||||
{!loading && <div className="showPage">Page: {page}</div>}
|
<ul className="skeleton-character-container">
|
||||||
|
{[...(data || Array(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>
|
||||||
|
) : (
|
||||||
|
<CharacterCard data={filteredData} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="changePage">
|
<div className="changePage">
|
||||||
<button className="button" disabled={page === 1} onClick={decrement}>
|
<button className="button" disabled={page === 1} onClick={decrement}>
|
||||||
|
|
Loading…
Reference in a new issue