Implemented phase 4 for the POC prototype in the Website service.

This commit is contained in:
javier committed 2026-09-21 12:55:35 +02:00
1 parent 95e0a455e8
commit 9403931109
42 files changed
+3407 -36

No files matched your search

@@ -0,0 +1,126 @@
/* CCN — grading guide. */
.guide,
.guide__section {
display: flex;
flex-direction: column;
}
.guide {
gap: var(--space-10);
}
.guide__head {
display: flex;
flex-direction: column;
gap: var(--space-3);
max-width: 760px;
}
.guide__scale {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
gap: var(--space-2);
}
.guide__grade {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: var(--space-2) var(--space-1);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--card);
color: var(--muted);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
}
.guide__grade strong {
color: var(--ink);
font-family: var(--font-display);
font-size: 18px;
letter-spacing: 0;
}
.guide__section {
gap: var(--space-5);
}
.guide__intro {
max-width: 720px;
color: var(--ink-soft);
}
.guide__references {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.guide__reference {
display: grid;
gap: var(--space-4);
align-items: start;
}
.guide__reference-grade {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.guide__photo {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 2 / 3;
max-width: 140px;
padding: var(--space-2);
border: 1px dashed var(--rule);
border-radius: var(--radius-sm);
color: var(--muted);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-align: center;
text-transform: uppercase;
}
.guide__reference .type-eyebrow {
margin-bottom: var(--space-2);
}
.guide__allowed {
color: var(--good);
}
.guide__excluded {
color: var(--bad);
}
.guide__defects {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: var(--space-4);
}
.guide__defect {
display: flex;
flex-direction: column;
gap: 6px;
padding: var(--space-4);
}
@media (min-width: 1024px) {
.guide {
padding-top: var(--space-10);
}
.guide__reference {
grid-template-columns: 180px 140px minmax(0, 1fr) minmax(0, 1fr);
gap: var(--space-6);
}
}
@@ -21,6 +21,13 @@
box-shadow: var(--shadow-cover);
}
.listing__photos {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-3);
padding-top: var(--space-4);
}
.listing__main {
display: flex;
flex: 1 1 auto;
@@ -1541,6 +1541,16 @@ textarea.input {
padding-left: 16cqw;
}
/* A photo of a copy, cropped to a cover's proportions. */
.photo {
display: block;
width: 100%;
aspect-ratio: 2 / 3;
border-radius: var(--radius-sm);
background: var(--tint);
object-fit: cover;
}
/* Listing cards */
.listing-card {
position: relative;
@@ -1747,6 +1757,13 @@ textarea.input {
font-weight: 500;
}
.select--block {
width: 100%;
height: var(--control-md);
border-color: var(--rule);
font-size: 14px;
}
.sort {
display: flex;
align-items: center;
@@ -69,6 +69,12 @@
gap: var(--space-7);
}
.gate__demo {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.gate__saved {
display: flex;
align-items: center;
@@ -782,3 +782,117 @@ a.rows__row:hover {
grid-template-columns: 100px minmax(0, 2fr) 70px minmax(0, 1fr) 340px 130px;
}
}
/* ==========================================================================
New listing
========================================================================== */
.listing-form,
.listing-form__form,
.listing-form__section,
.listing-form__scope {
display: flex;
flex-direction: column;
}
.listing-form {
gap: var(--space-6);
max-width: calc(880px + 2 * var(--gutter));
}
.listing-form__head {
display: flex;
flex-direction: column;
gap: 6px;
}
.listing-form__form {
gap: var(--space-5);
}
.listing-form__section {
gap: var(--space-5);
}
.listing-form__scope {
gap: var(--space-4);
}
.listing-form__grid {
display: grid;
gap: var(--space-4);
align-items: start;
}
.listing-form__modes {
display: grid;
gap: var(--space-3);
}
.listing-form__mode {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--rule);
border-radius: var(--radius-sm);
cursor: pointer;
}
.listing-form__mode:has(input:checked) {
border-color: var(--ink);
background: var(--tint);
}
.listing-form__mode span {
display: flex;
flex-direction: column;
gap: var(--space-1);
font-size: 14px;
}
.listing-form__checks {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 10px var(--space-4);
}
.listing-form__files {
padding: var(--space-4);
border: 1px dashed var(--rule);
border-radius: var(--radius-sm);
background: var(--paper);
font-size: 13px;
}
.listing-form__submit {
display: flex;
justify-content: flex-end;
gap: var(--space-3);
}
.sell__actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
@media (min-width: 600px) {
.listing-form__grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.listing-form__modes {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.field--wide {
grid-column: span 2;
}
}
@media (min-width: 1024px) {
.listing-form {
padding-top: var(--space-8);
}
}
@@ -0,0 +1,60 @@
// CCN — passkey sign-in. Without it, or without WebAuthn, the sign-in screen offers the demo sign-in alone.
const form = document.querySelector("[data-passkey-form]");
const supported = window.PublicKeyCredential
&& PublicKeyCredential.parseCreationOptionsFromJSON
&& PublicKeyCredential.parseRequestOptionsFromJSON;
if (form && supported) {
const button = form.querySelector("[data-passkey]");
const status = form.querySelector("[data-passkey-status]");
const post = async (path, body) => {
const response = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const answer = await response.json();
if (!response.ok) {
throw new Error(answer.error ?? "Something went wrong. Try again.");
}
return answer;
};
button.hidden = false;
button.addEventListener("click", async () => {
button.disabled = true;
status.textContent = "";
try {
const { mode, options } = await post("/sign-in/passkey/options", { email: form.elements.email.value });
let credential;
if (mode === "create") {
// A discoverable passkey, so the next sign-in needs no email.
options.authenticatorSelection = { residentKey: "required", userVerification: "preferred" };
credential = await navigator.credentials.create({
publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(options),
});
} else {
credential = await navigator.credentials.get({
publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(options),
});
}
const { location } = await post("/sign-in/passkey/finish", {
credential: credential.toJSON(),
return: form.elements.return.value,
});
window.location.assign(location);
} catch (error) {
// Dismissing the browser's prompt is not an error worth a message.
status.textContent = error.name === "NotAllowedError" ? "" : error.message;
button.disabled = false;
}
});
}
@@ -21,3 +21,22 @@ if (filters) {
sync();
desktop.addEventListener("change", sync);
}
// The new-listing form shows every field without scripts; here, only those that apply to the format and grading picked.
const listingForm = document.querySelector(".listing-form__form");
if (listingForm) {
const apply = () => {
const format = listingForm.querySelector("input[name=format]:checked")?.value;
const grading = listingForm.querySelector("input[name=grading]:checked")?.value;
const single = format === "single-issues";
const scopes = { "single-issues": single, collected: !single, slabbed: single && grading === "slabbed" };
for (const element of listingForm.querySelectorAll("[data-scope]")) {
element.hidden = !scopes[element.dataset.scope];
}
};
listingForm.addEventListener("change", apply);
apply();
}