Replace Payload with Elysia admin
Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
output: "static"
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@pozor/site",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "cross-env ASTRO_TELEMETRY_DISABLED=1 node ./node_modules/astro/astro.js dev --host 0.0.0.0 --port 4321",
|
||||
"build": "cross-env ASTRO_TELEMETRY_DISABLED=1 node ./node_modules/astro/astro.js build",
|
||||
"preview": "cross-env ASTRO_TELEMETRY_DISABLED=1 node ./node_modules/astro/astro.js preview --host 0.0.0.0 --port 4321"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^5.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^10.1.0",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
|
||||
type BoardCard = {
|
||||
id: string | number;
|
||||
title: string;
|
||||
comment?: string | null;
|
||||
imageUrl?: string | null;
|
||||
published?: boolean;
|
||||
};
|
||||
|
||||
type ApiResponse = {
|
||||
docs?: BoardCard[];
|
||||
};
|
||||
|
||||
type BoardItem = {
|
||||
id: string | number;
|
||||
title: string;
|
||||
comment?: string | null;
|
||||
image: {
|
||||
alt: string;
|
||||
url: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
const apiUrl = (
|
||||
import.meta.env.API_URL ||
|
||||
import.meta.env.PUBLIC_API_URL ||
|
||||
"http://localhost:3001"
|
||||
).replace(/\/$/, "");
|
||||
|
||||
async function getCards() {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/api/cards`);
|
||||
|
||||
if (!response.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ApiResponse;
|
||||
return data.docs ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function absolutize(url: string) {
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return `${apiUrl}${url.startsWith("/") ? "" : "/"}${url}`;
|
||||
}
|
||||
|
||||
function getImage(card: BoardCard) {
|
||||
if (card.imageUrl) {
|
||||
return {
|
||||
alt: card.title,
|
||||
url: absolutize(card.imageUrl)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const cards = await getCards();
|
||||
const boardItems: BoardItem[] = cards.map((card) => ({
|
||||
id: card.id,
|
||||
title: card.title,
|
||||
comment: card.comment,
|
||||
image: getImage(card)
|
||||
}));
|
||||
const boardStamp = boardItems.length === 0 ? "Empty" : `${boardItems.length} filed`;
|
||||
const adminUrl = `${apiUrl}/admin`;
|
||||
---
|
||||
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Доска позора</title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell" aria-labelledby="page-title">
|
||||
<section class="hero">
|
||||
<div class="heroText">
|
||||
<p class="sectionCode">Public board</p>
|
||||
<h1 id="page-title">Доска позора</h1>
|
||||
<p class="summary">тут только самое позорное.</p>
|
||||
</div>
|
||||
<aside class="caseTag" aria-label="Board status">
|
||||
<span>{boardStamp}</span>
|
||||
<strong>Elysia API</strong>
|
||||
<a class="loginButton" href={adminUrl}>Login</a>
|
||||
<small>admin / cards</small>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="board" aria-label="Published cards">
|
||||
{boardItems.length === 0 && <div class="notice">В админке пока нет опубликованных карточек.</div>}
|
||||
|
||||
{
|
||||
boardItems.map((item, index) => {
|
||||
return (
|
||||
<article class="photoCard">
|
||||
{item.image?.url && <img src={item.image.url} alt={item.image.alt} loading={index < 3 ? "eager" : "lazy"} />}
|
||||
<div class="caption">
|
||||
<span>#{String(index + 1).padStart(2, "0")}</span>
|
||||
<strong>{item.title}</strong>
|
||||
</div>
|
||||
{item.comment && <p class="cardComment">{item.comment}</p>}
|
||||
</article>
|
||||
);
|
||||
})
|
||||
}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,321 @@
|
||||
/* Hallmark · black-and-white editorial board */
|
||||
:root {
|
||||
--color-black: #000000;
|
||||
--color-ink: #f7f7f7;
|
||||
--color-paper: #090909;
|
||||
--color-panel: #101010;
|
||||
--color-panel-raised: #171717;
|
||||
--color-line: #f7f7f7;
|
||||
--color-line-soft: #5d5d5d;
|
||||
--color-muted: #a8a8a8;
|
||||
--color-faint: #2a2a2a;
|
||||
--shadow-hard: 10px 10px 0 var(--color-ink);
|
||||
--shadow-card: 6px 6px 0 var(--color-ink);
|
||||
--font-display: Georgia, "Times New Roman", serif;
|
||||
--font-body: Georgia, "Times New Roman", serif;
|
||||
--font-mono: "Courier New", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
min-height: 100%;
|
||||
overflow-x: clip;
|
||||
color-scheme: dark;
|
||||
background:
|
||||
linear-gradient(90deg, var(--color-faint) 1px, transparent 1px) 0 0 / 32px 32px,
|
||||
linear-gradient(var(--color-faint) 1px, transparent 1px) 0 0 / 32px 32px,
|
||||
var(--color-black);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
overflow-x: clip;
|
||||
background: var(--color-black);
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(1480px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 28px 0 60px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 360px);
|
||||
min-height: 310px;
|
||||
border: 2px solid var(--color-line);
|
||||
background: var(--color-panel);
|
||||
color: var(--color-ink);
|
||||
box-shadow: var(--shadow-hard);
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 16px;
|
||||
border: 1px dashed var(--color-line-soft);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: auto 0 0;
|
||||
height: 12px;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
var(--color-ink) 0,
|
||||
var(--color-ink) 20px,
|
||||
var(--color-black) 20px,
|
||||
var(--color-black) 40px
|
||||
);
|
||||
}
|
||||
|
||||
.heroText {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 34px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sectionCode {
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--color-line);
|
||||
color: var(--color-black);
|
||||
background: var(--color-ink);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 940px;
|
||||
min-width: 0;
|
||||
margin: 34px 0 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(4rem, 11vw, 10rem);
|
||||
line-height: 0.78;
|
||||
letter-spacing: 0;
|
||||
color: var(--color-ink);
|
||||
text-transform: uppercase;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.summary {
|
||||
max-width: 680px;
|
||||
margin: 28px 0 0;
|
||||
color: var(--color-muted);
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.52;
|
||||
}
|
||||
|
||||
.caseTag {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto auto;
|
||||
gap: 18px;
|
||||
min-height: 100%;
|
||||
padding: 28px;
|
||||
border-left: 2px solid var(--color-line);
|
||||
background: var(--color-black);
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.caseTag span {
|
||||
color: var(--color-black);
|
||||
background: var(--color-ink);
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
padding: 5px 7px;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.caseTag strong {
|
||||
align-self: center;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 1.35rem;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.loginButton {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
border: 2px solid var(--color-line);
|
||||
background: var(--color-ink);
|
||||
color: var(--color-black);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
box-shadow: 5px 5px 0 var(--color-line-soft);
|
||||
transition: box-shadow 160ms ease, background 160ms ease, color 160ms ease;
|
||||
}
|
||||
|
||||
.loginButton:hover,
|
||||
.loginButton:focus-visible {
|
||||
background: var(--color-black);
|
||||
color: var(--color-ink);
|
||||
outline: 0;
|
||||
box-shadow: 7px 7px 0 var(--color-ink);
|
||||
}
|
||||
|
||||
.caseTag small {
|
||||
color: var(--color-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
|
||||
gap: 28px;
|
||||
align-items: stretch;
|
||||
min-height: 240px;
|
||||
padding: 42px 0 12px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
grid-column: 1 / -1;
|
||||
padding: 24px;
|
||||
border: 2px solid var(--color-line);
|
||||
background: var(--color-panel);
|
||||
box-shadow: var(--shadow-card);
|
||||
color: var(--color-muted);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.photoCard {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
min-height: 100%;
|
||||
border: 2px solid var(--color-line);
|
||||
background: var(--color-panel);
|
||||
box-shadow: var(--shadow-card);
|
||||
transition: box-shadow 160ms ease, background 160ms ease;
|
||||
}
|
||||
|
||||
.photoCard:hover {
|
||||
background: var(--color-panel-raised);
|
||||
box-shadow: 9px 9px 0 var(--color-ink);
|
||||
}
|
||||
|
||||
.photoCard img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-bottom: 2px solid var(--color-line);
|
||||
background: var(--color-black);
|
||||
}
|
||||
|
||||
.caption {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--color-line-soft);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.caption span {
|
||||
padding: 4px 6px;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-black);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.caption strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--color-ink);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.cardComment {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.shell {
|
||||
width: min(100% - 20px, 720px);
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.heroText {
|
||||
padding: 26px 22px 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(3.2rem, 17vw, 6rem);
|
||||
}
|
||||
|
||||
.summary {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.caseTag {
|
||||
min-height: 132px;
|
||||
border-top: 2px solid var(--color-line);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.board {
|
||||
padding-top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.shell {
|
||||
width: min(100% - 16px, 420px);
|
||||
}
|
||||
|
||||
.hero,
|
||||
.photoCard,
|
||||
.notice {
|
||||
box-shadow: 5px 5px 0 var(--color-ink);
|
||||
}
|
||||
|
||||
.board {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user