Files
pozor/app/page.tsx
T
2026-06-05 02:54:22 +03:00

165 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { FormEvent, useEffect, useMemo, useState } from "react";
import Image from "next/image";
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;
};
const initialRepo = process.env.NEXT_PUBLIC_GITHUB_REPO ?? "";
const initialPath = process.env.NEXT_PUBLIC_GITHUB_PHOTOS_PATH ?? "";
const initialBranch = process.env.NEXT_PUBLIC_GITHUB_BRANCH ?? "";
function buildQuery(repo: string, path: string, branch: string) {
const query = new URLSearchParams();
if (repo.trim()) {
query.set("repo", repo.trim());
}
if (path.trim()) {
query.set("path", path.trim().replace(/^\/+/, ""));
}
if (branch.trim()) {
query.set("branch", branch.trim());
}
return query.toString();
}
export default function Home() {
const [repo, setRepo] = useState(initialRepo);
const [path, setPath] = useState(initialPath);
const [branch, setBranch] = useState(initialBranch);
const [activeQuery, setActiveQuery] = useState(() => buildQuery(initialRepo, initialPath, initialBranch));
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?${activeQuery}`, {
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();
}, [activeQuery]);
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]);
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setActiveQuery(buildQuery(repo, path, branch));
}
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>
<form className="sourceBar" onSubmit={handleSubmit}>
<label>
<span>repo</span>
<input
value={repo}
onChange={(event) => setRepo(event.target.value)}
spellCheck={false}
/>
</label>
<label>
<span>folder</span>
<input
value={path}
onChange={(event) => setPath(event.target.value)}
spellCheck={false}
/>
</label>
<label>
<span>branch</span>
<input
value={branch}
onChange={(event) => setBranch(event.target.value)}
spellCheck={false}
/>
</label>
<button type="submit">Обновить</button>
</form>
<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} style={{ "--tilt": `${(index % 5) - 2}deg` } as React.CSSProperties}>
<a href={photo.htmlUrl} target="_blank" rel="noreferrer" aria-label={`Open ${photo.name} on GitHub`}>
<Image src={photo.url} alt={photo.name} fill sizes="(max-width: 520px) 100vw, (max-width: 1200px) 33vw, 260px" />
</a>
<div className="caption">
<span>#{String(index + 1).padStart(2, "0")}</span>
<strong>{photo.name}</strong>
</div>
</article>
))}
</section>
</main>
);
}