initial shame board
This commit is contained in:
+164
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user