Skip to content

Commit

Permalink
Merge pull request #151 from Miruro-no-kuon/test-alfa
Browse files Browse the repository at this point in the history
v0.2.0
  • Loading branch information
Wannabe authored Feb 7, 2024
2 parents 6601e85 + f21b863 commit 9d4dea0
Show file tree
Hide file tree
Showing 23 changed files with 571 additions and 420 deletions.
34 changes: 22 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
# VITE_BACKEND_URL: The base URL of the primary backend server.
# Set this to the URL of your primary backend server in a production environment.
# Example: VITE_BACKEND_URL="https://api.consumet.org"
VITE_BACKEND_URL="https://api.consumet.org"
# VITE_BACKEND_URL: This is the base URL for the primary backend server.
# It is used for making API requests to fetch anime data, metadata, and other related information.
# Example: VITE_BACKEND_URL="https://api.consumet.org/"
VITE_BACKEND_URL="https://api.consumet.org/"

# VITE_BACKEND_URL_2: The base URL of a secondary backend server (if applicable).
# You can use a secondary backend server for specific features or redundancy.
# Set this to the URL of your secondary backend server in a production environment.
# VITE_BACKEND_URL_2: This represents the URL of a secondary or alternative backend server.
# It is used primarily for fetching episodes and related data.
# Typically points to a local server during development. Adjust as necessary for production.
# Example: VITE_BACKEND_URL_2="https://api.anify.tv/"
# Note: You have a commented out local development URL, which is useful for testing locally.
# Example (local development): VITE_BACKEND_URL_2="http://localhost:3060/"
VITE_BACKEND_URL_2="https://api.anify.tv/"

# VITE_API_KEY: Your API key for authentication with the backend servers.
# Set this to your actual API key in a production environment.
# VITE_API_KEY: A specific key required for accessing certain backend services or APIs.
# This key might be used for authentication or tracking API usage.
# Example: VITE_API_KEY="12345678-12345678-12345678"
VITE_API_KEY=""
VITE_API_KEY="12345678-12345678-12345678"

# VITE_PROXY_URL: The URL of the proxy server used in the application.
# This is essential for circumventing CORS issues or when making requests to external services that don't support CORS.
# Example: VITE_PROXY_URL="https://corsproxy.io"
VITE_PROXY_URL="https://corsproxy.io"

# VITE_IS_LOCAL: A flag to determine if the app is running in a local development environment.
# This can change the behavior of certain parts of the application, such as API endpoints or debugging features.
# It's typically set to "true" during development and "false" in production.
# Example: VITE_IS_LOCAL="false"
VITE_IS_LOCAL="false"

# PORT: The port number on which your server (if applicable) should listen.
# Set this to the desired port number in a production environment.
# It's important for configuring the server's listening port, especially in a local development environment.
# Example: PORT=5173
PORT=5173
29 changes: 26 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Miruro no Kuon" />
<meta
name="description"
content="Miruro no Kuon - Anime Streaming Site with minimal UI 🍜. Enjoy HD fast streaming of your favorite anime, manga reading, and explore anime-related forums on www.miruro.tv. Discover a world of anime entertainment at Miruro no Kuon!"
/>
<link
rel="icon"
type="image/svg+xml"
href="/src/assets/favicon white/android-chrome-512x512.png"
href="src/assets/favicon notr/favicon-circular-miruro-512x512.png"
/>
<link
rel="stylesheet"
Expand All @@ -17,10 +20,30 @@
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<title>Miruro no Kuon</title>
<title>Miruro</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script>
const themePreference = localStorage.getItem("themePreference");

if (themePreference === "dark") {
document.body.style.backgroundColor = "#080808";
} else if (themePreference === "light") {
document.body.style.backgroundColor = "#f5f5f5";
} else if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
document.body.style.backgroundColor = "#080808"; // Set to dark if user prefers dark theme
} else {
document.body.style.backgroundColor = "#f5f5f5"; // Set to light if none of the above conditions match
}

document.addEventListener("DOMContentLoaded", function () {
document.body.style.backgroundColor = "";
});
</script>
</body>
</html>
2 changes: 0 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import Home from "./pages/Home";
import Watch from "./pages/Watch";
import SearchResults from "./pages/SearchResults";
import PageNotFound from "./pages/404";
import ScrollToTopButton from "./components/ScrollUp";

function ScrollToTop() {
const { pathname } = useLocation();
Expand Down Expand Up @@ -48,7 +47,6 @@ function App() {
<Route path="/watch/:animeId" element={<Watch />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
<ScrollToTopButton />
<Footer />
</Router>
);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/miruro-banner-dark-bg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/miruro-black-resized.webp
Binary file not shown.
41 changes: 21 additions & 20 deletions src/components/Cards/CardGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,58 @@ import { faArrowCircleDown } from "@fortawesome/free-solid-svg-icons";
const CardGrid = ({ animeData, totalPages, hasNextPage, onLoadMore }) => {
const [loading, setLoading] = useState(true);
const [showLoadMoreButton, setShowLoadMoreButton] = useState(false);
const [hoveredCard, setHoveredCard] = useState(null);
const [hoveredCardInstant, setHoveredCardInstant] = useState(null);
const [hoveredCardDelayed, setHoveredCardDelayed] = useState(null);
const [loadMoreClicked, setLoadMoreClicked] = useState(false);
const hoverTimeouts = useRef({});

useEffect(() => {
const loadingTimeout = setTimeout(() => {
setLoading(false);
setShowLoadMoreButton(true);
setLoadMoreClicked(false); // Reset the load more click state when new data is loaded
setLoadMoreClicked(false);
}, 0);

return () => clearTimeout(loadingTimeout);
}, [animeData]); // Update the dependency array to include animeData

const handleLoadMoreClick = () => {
setLoadMoreClicked(true); // Disable the button once clicked
onLoadMore();
};
}, [animeData]);

const handleCardHover = useMemo(
() => (animeId) => {
setHoveredCardInstant(animeId); // Set instant hover state

if (hoverTimeouts.current[animeId]) {
clearTimeout(hoverTimeouts.current[animeId]);
}

hoverTimeouts.current[animeId] = setTimeout(() => {
if (hoveredCard !== animeId) {
setHoveredCard(animeId);
}
}, 0);
setHoveredCardDelayed(animeId); // Set delayed hover state
}, 200);
},
[hoveredCard]
[]
);

const handleCardLeave = useMemo(
() => (animeId) => {
setHoveredCardInstant(null); // Clear instant hover state

if (hoverTimeouts.current[animeId]) {
clearTimeout(hoverTimeouts.current[animeId]);
}
if (hoveredCard !== null) {
setHoveredCard(null);
}
setHoveredCardDelayed(null); // Clear delayed hover state
},
[hoveredCard]
[]
);

const handleLoadMoreClick = () => {
setLoadMoreClicked(true);
onLoadMore();
};

const renderLoadMoreButton = () => {
if (hasNextPage && showLoadMoreButton) {
return (
<LoadMoreButton
onClick={handleLoadMoreClick}
disabled={loading || loadMoreClicked} // Button is disabled when loading or already clicked
disabled={loading || loadMoreClicked}
>
<FontAwesomeIcon icon={faArrowCircleDown} className="icon" />
<LoadMoreText>
Expand All @@ -83,7 +83,8 @@ const CardGrid = ({ animeData, totalPages, hasNextPage, onLoadMore }) => {
anime={anime}
onHover={() => handleCardHover(anime.id)}
onLeave={() => handleCardLeave(anime.id)}
isHovered={hoveredCard === anime.id}
isHoveredInstant={hoveredCardInstant === anime.id}
isHoveredDelayed={hoveredCardDelayed === anime.id}
/>
))}
{renderLoadMoreButton()}
Expand Down
167 changes: 92 additions & 75 deletions src/components/Cards/CardItem.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from "react";
import React, { useEffect, useRef, useState } from "react";
import styled, { keyframes } from "styled-components";
import { useNavigate } from "react-router-dom";
import ImageDisplay from "./ImageDisplay";
Expand Down Expand Up @@ -34,88 +34,105 @@ const StyledCardItem = styled.div`
transition: 0.2s;
`;

const CardItemContent = React.memo(({ anime, onHover, onLeave, isHovered }) => {
const { width } = useWindowDimensions();
const cardRef = useRef(null);
const navigate = useNavigate();
const CardItemContent = React.memo(
({ anime, onHover, onLeave, isHoveredInstant, isHoveredDelayed }) => {
const [isMobile, setIsMobile] = useState(isMobileDevice());
const { width } = useWindowDimensions();
const cardRef = useRef(null);
const navigate = useNavigate();

useEffect(() => {
if (cardRef.current) {
const cardPosition = getElementPosition(cardRef.current);
const isLeft = cardPosition.left < width / 2;
}
}, [width]);
useEffect(() => {
const handleResize = () => {
setIsMobile(isMobileDevice());
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);

useEffect(() => {
if (cardRef.current) {
const cardPosition = getElementPosition(cardRef.current);
const isLeft = cardPosition.left < width / 2;
}
}, [width]);

const {
coverImage,
bannerImage,
releaseDate,
popularity,
format,
type,
totalEpisodes,
currentEpisode,
description,
genres,
} = anime;
const {
coverImage,
bannerImage,
releaseDate,
popularity,
format,
type,
totalEpisodes,
currentEpisode,
description,
genres,
} = anime;

const altText = anime.title?.romaji || anime.title?.english;
const altText = anime.title?.romaji || anime.title?.english;

// Use rating.anilist if available, otherwise null
const ratingValue =
typeof anime.rating === "number"
? anime.rating
: anime.rating?.anilist ?? null;
// Use rating.anilist if available, otherwise null
const ratingValue =
typeof anime.rating === "number"
? anime.rating
: anime.rating?.anilist ?? null;

const isMobile = width <= 1000;
function isMobileDevice() {
const userAgent =
typeof window.navigator === "undefined" ? "" : navigator.userAgent;
const mobileRegex =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
return mobileRegex.test(userAgent) || window.innerWidth <= 768;
}

const handleCardClick = () => {
navigate(`/watch/${anime.id}`);
};
const handleCardClick = () => {
navigate(`/watch/${anime.id}`);
};

return (
<StyledCardWrapper
onClick={handleCardClick}
onMouseEnter={onHover}
onMouseLeave={onLeave}
color={anime.color}
>
<StyledCardItem ref={cardRef}>
<ImageDisplay
imageSrc={anime.coverImage || anime.image}
altText={altText}
type={format || type}
totalEpisodes={totalEpisodes}
rating={ratingValue}
color={anime.color}
$ishovered={isHovered}
/>
return (
<StyledCardWrapper
onClick={handleCardClick}
onMouseEnter={onHover}
onMouseLeave={onLeave}
color={anime.color}
>
<StyledCardItem ref={cardRef}>
<ImageDisplay
imageSrc={anime.coverImage || anime.image}
altText={altText}
type={format || type}
totalEpisodes={totalEpisodes}
rating={ratingValue}
color={anime.color}
$ishovered={isHoveredInstant}
/>

<TitleComponent $ishovered={isHovered} anime={anime} />
</StyledCardItem>
<TitleComponent $ishovered={isHoveredInstant} anime={anime} />
</StyledCardItem>

{!isMobile && isHovered && (
<InfoPopupContent
title={altText}
description={description}
genres={genres}
$isPositionedLeft={
cardRef.current &&
getElementPosition(cardRef.current).left < width / 2
}
color={anime.color}
type={format || type}
status={anime.status}
popularity={popularity?.anidb || popularity}
totalEpisodes={totalEpisodes}
currentEpisode={currentEpisode}
releaseDate={releaseDate || anime.year}
cover={bannerImage || anime.cover}
maxDescriptionLength={100}
/>
)}
</StyledCardWrapper>
);
});
{!isMobile && isHoveredDelayed && (
<InfoPopupContent
title={altText}
description={description}
genres={genres}
$isPositionedLeft={
cardRef.current &&
getElementPosition(cardRef.current).left < width / 2
}
color={anime.color}
type={format || type}
status={anime.status}
popularity={popularity?.anidb || popularity}
totalEpisodes={totalEpisodes}
currentEpisode={currentEpisode}
releaseDate={releaseDate || anime.year}
cover={bannerImage || anime.cover}
maxDescriptionLength={100}
/>
)}
</StyledCardWrapper>
);
}
);

export default CardItemContent;
Loading

0 comments on commit 9d4dea0

Please sign in to comment.