105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useMemo, useState } from "react";
|
||
|
||
type ShamePhoto = {
|
||
name: string;
|
||
path: string;
|
||
url: string;
|
||
htmlUrl: string;
|
||
};
|
||
|
||
type PhotoResponse = {
|
||
photos: ShamePhoto[];
|
||
repo: string | null;
|
||
path: string;
|
||
branch?: string | null;
|
||
message: string | null;
|
||
};
|
||
|
||
export default function Home() {
|
||
const [data, setData] = useState<PhotoResponse | null>(null);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
const controller = new AbortController();
|
||
|
||
async function loadPhotos() {
|
||
setIsLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
const response = await fetch("/api/github-photos", {
|
||
signal: controller.signal
|
||
});
|
||
const nextData = (await response.json()) as PhotoResponse;
|
||
|
||
setData(nextData);
|
||
|
||
if (!response.ok) {
|
||
setError(nextData.message || "Could not load photos from GitHub.");
|
||
}
|
||
} catch (nextError) {
|
||
if (nextError instanceof DOMException && nextError.name === "AbortError") {
|
||
return;
|
||
}
|
||
|
||
setError("Could not reach the photo source.");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
|
||
loadPhotos();
|
||
|
||
return () => controller.abort();
|
||
}, []);
|
||
|
||
const repoLabel = data?.repo ?? "GitHub source not set";
|
||
const boardStamp = useMemo(() => {
|
||
const count = data?.photos.length ?? 0;
|
||
|
||
if (count === 0) {
|
||
return "Empty";
|
||
}
|
||
|
||
return `${count} filed`;
|
||
}, [data?.photos.length]);
|
||
|
||
return (
|
||
<main className="shell">
|
||
<section className="hero" aria-labelledby="page-title">
|
||
<div className="heroText">
|
||
<h1 id="page-title">Доска позора</h1>
|
||
<p className="summary">
|
||
тут только самое позорное.
|
||
</p>
|
||
</div>
|
||
<div className="caseTag" aria-label="Board status">
|
||
<span>{boardStamp}</span>
|
||
<strong>{repoLabel}</strong>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="board" aria-live="polite" aria-busy={isLoading}>
|
||
{isLoading ? <div className="notice">Загружаю фотографии с GitHub...</div> : null}
|
||
{!isLoading && error ? <div className="notice danger">{error}</div> : null}
|
||
{!isLoading && !error && data?.message ? <div className="notice">{data.message}</div> : null}
|
||
|
||
{data?.photos.map((photo, index) => (
|
||
<article className="photoCard" key={photo.path}>
|
||
<a href={photo.htmlUrl} target="_blank" rel="noreferrer" aria-label={`Open ${photo.name} on GitHub`}>
|
||
<img src={photo.url} alt={photo.name} loading={index < 3 ? "eager" : "lazy"} />
|
||
</a>
|
||
<div className="caption">
|
||
<span>#{String(index + 1).padStart(2, "0")}</span>
|
||
<strong>{photo.name}</strong>
|
||
</div>
|
||
</article>
|
||
))}
|
||
</section>
|
||
</main>
|
||
);
|
||
}
|