initial shame board

This commit is contained in:
dexx
2026-06-05 02:48:52 +03:00
parent c71ef3a8ef
commit 6848cb66a7
13 changed files with 1488 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
"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>
);
}