configure board source in typescript

This commit is contained in:
dexx
2026-06-05 03:02:41 +03:00
parent 1bafc85226
commit 92cf1eaa63
4 changed files with 23 additions and 143 deletions
+5 -6
View File
@@ -1,4 +1,5 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { boardConfig } from "../../../board.config";
type GithubContent = { type GithubContent = {
name: string; name: string;
@@ -68,12 +69,10 @@ async function fetchDirectory(owner: string, repo: string, path: string, branch?
}; };
} }
export async function GET(request: Request) { export async function GET() {
const { searchParams } = new URL(request.url); const branch = boardConfig.branch || undefined;
const repoValue = searchParams.get("repo") || process.env.NEXT_PUBLIC_GITHUB_REPO || null; const path = boardConfig.photosPath;
const branch = searchParams.get("branch") || process.env.NEXT_PUBLIC_GITHUB_BRANCH || undefined; const parsedRepo = parseRepo(boardConfig.repo);
const path = searchParams.get("path") || process.env.NEXT_PUBLIC_GITHUB_PHOTOS_PATH || "";
const parsedRepo = parseRepo(repoValue);
if (!parsedRepo) { if (!parsedRepo) {
return NextResponse.json({ return NextResponse.json({
+8 -72
View File
@@ -131,71 +131,13 @@ h1 {
line-height: 1.1; line-height: 1.1;
} }
.sourceBar {
display: grid;
grid-template-columns: minmax(190px, 1.2fr) minmax(160px, 0.8fr) minmax(130px, 0.55fr) auto;
gap: 12px;
align-items: end;
margin: 34px 0 28px;
padding: 14px;
border: 2px solid var(--line);
background: rgba(12, 15, 12, 0.94);
box-shadow: 6px 6px 0 var(--shadow);
}
.sourceBar label {
display: grid;
gap: 7px;
min-width: 0;
}
.sourceBar span {
font-family: "Courier New", monospace;
font-size: 0.75rem;
font-weight: 800;
text-transform: uppercase;
}
.sourceBar input {
width: 100%;
min-height: 46px;
border: 2px solid var(--line);
border-radius: 0;
background: #050605;
color: var(--ink);
padding: 0 12px;
outline: none;
}
.sourceBar input:focus {
box-shadow: 0 0 0 3px var(--acid);
}
.sourceBar button {
min-height: 46px;
border: 2px solid var(--line);
border-radius: 0;
background: var(--blood);
color: white;
cursor: pointer;
font-family: "Courier New", monospace;
font-weight: 900;
text-transform: uppercase;
transition: transform 160ms ease, box-shadow 160ms ease;
}
.sourceBar button:hover {
box-shadow: 4px 4px 0 var(--line);
transform: translate(-2px, -2px);
}
.board { .board {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 26px; gap: 26px;
align-items: start; align-items: start;
min-height: 240px; min-height: 240px;
padding: 12px 0; padding: 34px 0 12px;
} }
.notice { .notice {
@@ -214,24 +156,20 @@ h1 {
} }
.photoCard { .photoCard {
--tilt: 0deg;
border: 2px solid var(--line); border: 2px solid var(--line);
background: #0d100d; background: #0d100d;
box-shadow: 8px 9px 0 var(--line); box-shadow: 8px 9px 0 var(--line);
transform: rotate(var(--tilt));
transition: transform 180ms ease, box-shadow 180ms ease; transition: transform 180ms ease, box-shadow 180ms ease;
} }
.photoCard:hover { .photoCard:hover {
box-shadow: 12px 13px 0 var(--blood); box-shadow: 12px 13px 0 var(--blood);
transform: rotate(0deg) translate(-3px, -3px); transform: translate(-3px, -3px);
} }
.photoCard a { .photoCard a {
display: block; display: block;
position: relative; overflow: visible;
aspect-ratio: 4 / 5;
overflow: hidden;
border-bottom: 2px solid var(--line); border-bottom: 2px solid var(--line);
background: #1b2019; background: #1b2019;
} }
@@ -239,8 +177,7 @@ h1 {
.photoCard img { .photoCard img {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: auto;
object-fit: cover;
filter: contrast(1.12) saturate(0.82) brightness(0.86); filter: contrast(1.12) saturate(0.82) brightness(0.86);
} }
@@ -275,8 +212,7 @@ h1 {
padding-top: 14px; padding-top: 14px;
} }
.hero, .hero {
.sourceBar {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -296,8 +232,8 @@ h1 {
min-height: 130px; min-height: 130px;
} }
.sourceBar button { .board {
width: 100%; padding-top: 24px;
} }
} }
+5 -65
View File
@@ -1,7 +1,6 @@
"use client"; "use client";
import { FormEvent, useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import Image from "next/image";
type ShamePhoto = { type ShamePhoto = {
name: string; name: string;
@@ -18,33 +17,7 @@ type PhotoResponse = {
message: 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() { 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 [data, setData] = useState<PhotoResponse | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -57,7 +30,7 @@ export default function Home() {
setError(null); setError(null);
try { try {
const response = await fetch(`/api/github-photos?${activeQuery}`, { const response = await fetch("/api/github-photos", {
signal: controller.signal signal: controller.signal
}); });
const nextData = (await response.json()) as PhotoResponse; const nextData = (await response.json()) as PhotoResponse;
@@ -81,7 +54,7 @@ export default function Home() {
loadPhotos(); loadPhotos();
return () => controller.abort(); return () => controller.abort();
}, [activeQuery]); }, []);
const repoLabel = data?.repo ?? "GitHub source not set"; const repoLabel = data?.repo ?? "GitHub source not set";
const boardStamp = useMemo(() => { const boardStamp = useMemo(() => {
@@ -94,11 +67,6 @@ export default function Home() {
return `${count} filed`; return `${count} filed`;
}, [data?.photos.length]); }, [data?.photos.length]);
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setActiveQuery(buildQuery(repo, path, branch));
}
return ( return (
<main className="shell"> <main className="shell">
<section className="hero" aria-labelledby="page-title"> <section className="hero" aria-labelledby="page-title">
@@ -114,43 +82,15 @@ export default function Home() {
</div> </div>
</section> </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}> <section className="board" aria-live="polite" aria-busy={isLoading}>
{isLoading ? <div className="notice">Загружаю фотографии с GitHub...</div> : null} {isLoading ? <div className="notice">Загружаю фотографии с GitHub...</div> : null}
{!isLoading && error ? <div className="notice danger">{error}</div> : null} {!isLoading && error ? <div className="notice danger">{error}</div> : null}
{!isLoading && !error && data?.message ? <div className="notice">{data.message}</div> : null} {!isLoading && !error && data?.message ? <div className="notice">{data.message}</div> : null}
{data?.photos.map((photo, index) => ( {data?.photos.map((photo, index) => (
<article className="photoCard" key={photo.path} style={{ "--tilt": `${(index % 5) - 2}deg` } as React.CSSProperties}> <article className="photoCard" key={photo.path}>
<a href={photo.htmlUrl} target="_blank" rel="noreferrer" aria-label={`Open ${photo.name} on GitHub`}> <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" /> <img src={photo.url} alt={photo.name} loading={index < 3 ? "eager" : "lazy"} />
</a> </a>
<div className="caption"> <div className="caption">
<span>#{String(index + 1).padStart(2, "0")}</span> <span>#{String(index + 1).padStart(2, "0")}</span>
+5
View File
@@ -0,0 +1,5 @@
export const boardConfig = {
repo: "dexxdbg/pozor",
photosPath: "img",
branch: "codex/initial-shame-board"
} as const;