mirror of
https://github.com/wgh136/nysoure.git
synced 2025-12-16 07:51:14 +00:00
281 lines
8.1 KiB
TypeScript
281 lines
8.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import ResourcesView from "../components/resources_view.tsx";
|
|
import { network } from "../network/network.ts";
|
|
import { app } from "../app.ts";
|
|
import { Resource, RSort, Statistics } from "../network/models.ts";
|
|
import { useTranslation } from "../utils/i18n";
|
|
import { useAppContext } from "../components/AppContext.tsx";
|
|
import Select from "../components/select.tsx";
|
|
import { useNavigate } from "react-router";
|
|
import { useNavigator } from "../components/navigator.tsx";
|
|
import {
|
|
MdOutlineAccessTime,
|
|
MdOutlineArchive,
|
|
MdOutlineClass,
|
|
} from "react-icons/md";
|
|
|
|
export default function HomePage() {
|
|
useEffect(() => {
|
|
document.title = app.appName;
|
|
}, []);
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const appContext = useAppContext();
|
|
|
|
const [order, setOrder] = useState(() => {
|
|
if (appContext && appContext.get("home_page_order") !== undefined) {
|
|
return appContext.get("home_page_order");
|
|
}
|
|
return RSort.TimeDesc;
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (appContext && order !== RSort.TimeDesc) {
|
|
appContext.set("home_page_order", order);
|
|
}
|
|
}, [appContext, order]);
|
|
|
|
return (
|
|
<>
|
|
<HomeHeader />
|
|
<div className={"flex pt-4 px-4 items-center"}>
|
|
<Select
|
|
values={[
|
|
t("Time Ascending"),
|
|
t("Time Descending"),
|
|
t("Views Ascending"),
|
|
t("Views Descending"),
|
|
t("Downloads Ascending"),
|
|
t("Downloads Descending"),
|
|
t("Release Date Ascending"),
|
|
t("Release Date Descending"),
|
|
]}
|
|
current={order}
|
|
onSelected={(index) => {
|
|
setOrder(index);
|
|
if (appContext) {
|
|
appContext.set("home_page_order", index);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
<ResourcesView
|
|
key={`home_page_${order}`}
|
|
storageKey={`home_page_${order}`}
|
|
loader={(page) => network.getResources(page, order)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function HomeHeader() {
|
|
const [pinnedResources, setPinnedResources] = useState<Resource[]>([]);
|
|
const [statistic, setStatistic] = useState<Statistics | null>(null);
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const navigator = useNavigator();
|
|
const appContext = useAppContext();
|
|
|
|
useEffect(() => {
|
|
const pinned = appContext.get("pinned_resources");
|
|
const stats = appContext.get("site_statistics");
|
|
if (pinned) {
|
|
setPinnedResources(pinned);
|
|
}
|
|
if (stats) {
|
|
setStatistic(stats);
|
|
}
|
|
if (pinned && stats) {
|
|
return;
|
|
}
|
|
|
|
const prefetchData = app.getPreFetchData();
|
|
if (prefetchData && prefetchData.background) {
|
|
navigator.setBackground(
|
|
network.getResampledImageUrl(prefetchData.background),
|
|
);
|
|
}
|
|
let ok1 = false;
|
|
let ok2 = false;
|
|
if (prefetchData && prefetchData.statistics) {
|
|
setStatistic(prefetchData.statistics);
|
|
appContext.set("site_statistics", prefetchData.statistics);
|
|
ok1 = true;
|
|
}
|
|
if (prefetchData && prefetchData.pinned) {
|
|
const r = prefetchData.pinned;
|
|
appContext.set("pinned_resources", r);
|
|
setPinnedResources(r!);
|
|
ok2 = true;
|
|
}
|
|
if (ok1 && ok2) {
|
|
return;
|
|
}
|
|
|
|
const fetchPinnedResources = async () => {
|
|
const res = await network.getPinnedResources();
|
|
if (res.success) {
|
|
appContext.set("pinned_resources", res.data);
|
|
setPinnedResources(res.data ?? []);
|
|
}
|
|
};
|
|
const fetchStatistics = async () => {
|
|
const res = await network.getStatistic();
|
|
if (res.success) {
|
|
appContext.set("site_statistics", res.data);
|
|
setStatistic(res.data!);
|
|
}
|
|
};
|
|
fetchPinnedResources();
|
|
fetchStatistics();
|
|
}, [appContext, navigator]);
|
|
|
|
// Auto-scroll carousel every 5 seconds
|
|
useEffect(() => {
|
|
if (pinnedResources.length <= 1) {
|
|
return;
|
|
}
|
|
|
|
const interval = setInterval(() => {
|
|
setCurrentIndex((prevIndex) => (prevIndex + 1) % pinnedResources.length);
|
|
}, 5000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [pinnedResources.length, currentIndex]);
|
|
|
|
if (pinnedResources.length == 0 || statistic == null) {
|
|
return <></>;
|
|
}
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 p-4 gap-4">
|
|
<PinnedResourcesCarousel
|
|
resources={pinnedResources}
|
|
currentIndex={currentIndex}
|
|
onIndexChange={setCurrentIndex}
|
|
/>
|
|
<div className={"hidden md:flex h-52 md:h-60 flex-col"}>
|
|
<div className={"card w-full shadow p-4 mb-4 bg-base-100-tr82 flex-1"}>
|
|
<h2 className={"text-lg font-bold pb-2"}>{app.appName}</h2>
|
|
<p className={"text-xs"}>{app.siteDescription}</p>
|
|
</div>
|
|
<StatisticCard statistic={statistic} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PinnedResourcesCarousel({
|
|
resources,
|
|
currentIndex,
|
|
onIndexChange,
|
|
}: {
|
|
resources: Resource[];
|
|
currentIndex: number;
|
|
onIndexChange: (index: number) => void;
|
|
}) {
|
|
return (
|
|
<div className="relative">
|
|
<div className="overflow-hidden rounded-2xl">
|
|
<div
|
|
className="flex transition-transform duration-500 ease-in-out"
|
|
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
|
|
>
|
|
{resources.map((resource) => (
|
|
<div key={resource.id} className="w-full flex-shrink-0">
|
|
<PinnedResourceItem resource={resource} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{resources.length > 1 && (
|
|
<div className="absolute bottom-2 left-1/2 transform -translate-x-1/2 flex gap-2 z-10">
|
|
{resources.map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => onIndexChange(index)}
|
|
className={`w-2 h-2 rounded-full transition-all ${
|
|
index === currentIndex
|
|
? "bg-white w-6"
|
|
: "bg-white/50 hover:bg-white/75"
|
|
}`}
|
|
aria-label={`Go to slide ${index + 1}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PinnedResourceItem({ resource }: { resource: Resource }) {
|
|
const navigate = useNavigate();
|
|
|
|
return (
|
|
<a
|
|
href={`/resources/${resource.id}`}
|
|
className={"cursor-pointer block"}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
navigate(`/resources/${resource.id}`);
|
|
}}
|
|
>
|
|
<div
|
|
className={
|
|
"shadow hover:shadow-md transition-shadow rounded-2xl overflow-clip relative"
|
|
}
|
|
>
|
|
{resource.image != null && (
|
|
<figure>
|
|
<img
|
|
src={network.getResampledImageUrl(resource.image.id)}
|
|
alt="cover"
|
|
className="w-full h-52 md:h-60 object-cover"
|
|
/>
|
|
</figure>
|
|
)}
|
|
<div className="p-4 absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent">
|
|
<h2 className="break-all card-title text-white">{resource.title}</h2>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
);
|
|
}
|
|
|
|
function StatisticCard({ statistic }: { statistic: Statistics }) {
|
|
const { t } = useTranslation();
|
|
|
|
const now = new Date();
|
|
const createdAt = new Date(statistic.start_time * 1000);
|
|
const diffTime = Math.abs(now.getTime() - createdAt.getTime());
|
|
const survivalTime = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
return (
|
|
<div className="stats shadow w-full bg-base-100-tr82">
|
|
<div className="stat">
|
|
<div className="stat-figure text-secondary pt-2">
|
|
<MdOutlineClass size={28} />
|
|
</div>
|
|
<div className="stat-title">{t("Resources")}</div>
|
|
<div className="stat-value">{statistic.total_resources}</div>
|
|
</div>
|
|
|
|
<div className="stat">
|
|
<div className="stat-figure text-secondary pt-2">
|
|
<MdOutlineArchive size={28} />
|
|
</div>
|
|
<div className="stat-title">{t("Files")}</div>
|
|
<div className="stat-value">{statistic.total_files}</div>
|
|
</div>
|
|
|
|
<div className="stat">
|
|
<div className="stat-figure text-accent pt-2">
|
|
<MdOutlineAccessTime size={28} />
|
|
</div>
|
|
<div className="stat-title">{t("Survival time")}</div>
|
|
<div className="stat-value">{survivalTime}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|