Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.2.0 #151

Merged
9 commits merged into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading