Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions apps/web/src/app/(site)/[locale]/account/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { notFound } from "next/navigation";
import { ProjectDetail } from "@/components/ProjectDetail";
import { getDict, isLocale, type Locale } from "@/lib/i18n";
import "../../../../prototype.css";
import "../../../../account.css";
import "../../../../project.css";

export default async function ProjectPage({
params,
Expand All @@ -13,8 +16,10 @@ export default async function ProjectPage({
const d = getDict(locale);

return (
<main className="wrap" style={{ padding: "40px 24px 72px" }}>
<ProjectDetail locale={locale} d={d} projectId={id} />
<main className="pj">
<div className="wrap">
<ProjectDetail locale={locale} d={d} projectId={id} />
</div>
</main>
);
}
15 changes: 12 additions & 3 deletions apps/web/src/app/(site)/[locale]/lab/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { LabPanel } from "@/components/LabPanel";
import { getDict, isLocale, type Locale } from "@/lib/i18n";
import "../../../prototype.css";
import "../../../lab.css";

// Reachable by URL, deliberately absent from the nav, and kept out of search results until
// it goes public.
Expand All @@ -18,9 +20,16 @@ export default async function LabPage({
const d = getDict(locale);

return (
<main className="wrap" style={{ padding: "40px 24px 72px" }}>
<h1>{d.lab.title}</h1>
<LabPanel locale={locale} d={d} />
<main className="lbp">
<div className="pp-band lbp-band">
<div className="wrap">
<h1>{d.lab.title}</h1>
<p className="lbp-lede">{d.lab.intro}</p>
</div>
</div>
<div className="wrap lbp-body">
<LabPanel locale={locale} d={d} />
</div>
</main>
);
}
1 change: 1 addition & 0 deletions apps/web/src/app/account.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* The customer's account: sign-in card, then projects, prototypes and ad accounts. */
.acc-page { background: var(--tint); min-height: 70vh; padding: 48px 0 80px; }
.acc-page .wrap { max-width: 1200px; }

/* ---------- Sign in ---------- */
.acc-signin { max-width: 440px; margin: 24px auto 0; background: #fff; border: 1px solid var(--border); border-radius: 20px; padding: 36px 32px; box-shadow: var(--shadow); text-align: center; }
Expand Down
63 changes: 63 additions & 0 deletions apps/web/src/app/lab.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* The ads lab (/[locale]/lab): head band, the generator form and the campaigns in white cards,
the rendered ads as a list. Scoped under `.lbp` (page) and `.lb` (panel). */

.lbp { padding-bottom: 80px; }
.lbp .wrap { max-width: 1200px; }
.lbp-band { padding: 40px 0 84px; }
.lbp-band h1 { font-size: clamp(2rem, 3.6vw, 2.8rem); letter-spacing: -0.04em; margin: 0 0 10px; }
.lbp .lbp-lede { font-size: 1.05rem; color: var(--ink-soft); margin: 0; }
.lbp-body { margin-top: -52px; }
.lbp .est-empty { background: #fff; border: 1px solid var(--border); border-radius: 16px; padding: 18px 22px; box-shadow: var(--shadow); }

.lb p { margin: 0; }
.lb-card { background: #fff; border: 1px solid var(--border); border-radius: 20px; padding: 26px 28px; box-shadow: 0 30px 70px rgba(15, 23, 42, 0.08); }
.lb-card h2 { font-size: 1.2rem; margin: 0; }

/* Generator form */
.lb-form { display: grid; gap: 18px; margin-bottom: 24px; }
.lb-field { display: grid; gap: 6px; font-size: 0.86rem; font-weight: 600; color: var(--ink); min-width: 0; }
.lb-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.lb-select { width: 100%; font: 500 0.92rem var(--font-body); color: var(--ink); background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2 4 4 4 4-4'/%3E%3C/svg%3E") no-repeat right 12px center; border: 1px solid #d5dae4; border-radius: 10px; padding: 10px 34px 10px 12px; appearance: none; -webkit-appearance: none; cursor: pointer; }
.lb-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(29, 78, 216, 0.12); }
.lb-submit { justify-self: start; }

.lb-media { display: block; width: 100%; max-width: 360px; border-radius: 16px; margin: 0 0 24px; box-shadow: 0 20px 50px rgba(15, 23, 42, 0.15); }
.lb .lb-empty { color: var(--ink-soft); font-size: 0.92rem; padding: 8px 0; }

/* Rendered ads */
.lb-list { list-style: none; margin: 0 0 32px; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.lb-list li { display: flex; align-items: center; gap: 14px; background: #fff; border: 1px solid var(--border); border-radius: 14px; padding: 12px 14px; box-shadow: var(--shadow); }
.lb-list-ico { flex: none; width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center; background: var(--accent-soft); color: var(--accent); }
.lb-list-ico .ico { width: 20px; height: 20px; }
.lb-list-text { flex: 1; min-width: 0; }
.lb-list-text b { display: block; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-list-text small { display: block; font-size: 0.8rem; color: var(--ink-soft); margin-top: 2px; }
.lb .btn { display: inline-flex; align-items: center; gap: 8px; }
.lb-ghost { flex: none; padding: 8px 16px; font-size: 0.88rem; background: #fff; color: var(--ink); border-color: var(--border); }
.lb-ghost:hover:not(:disabled) { border-color: #c7d2fe; color: var(--accent-hover); }
.lb .btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* Campaigns */
.lb-camps { display: grid; gap: 12px; }
.lb .lb-note { font-size: 0.9rem; color: var(--ink-soft); }
.lb-platforms { display: flex; flex-wrap: wrap; gap: 8px; }
.lb-platforms span { font-size: 0.8rem; font-weight: 600; padding: 4px 11px; border-radius: 99px; background: var(--tint); color: var(--ink-soft); }
.lb-platforms span.is-on { background: #dcfce7; color: var(--valid); }
.lb-camp-list { list-style: none; margin: 0; padding: 0; }
.lb-camp-list > li { padding: 14px 0; border-top: 1px solid var(--border); display: grid; gap: 8px; }
.lb-camp-head { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; }
.lb-camp-head small { font-size: 0.82rem; color: var(--ink-soft); }
.lb-problems { margin: 0; padding: 10px 14px 10px 30px; font-size: 0.84rem; color: var(--warn-ink); background: var(--warn-bg); border-radius: 10px; }
.lb-camp-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.lb-camp-actions .btn { padding: 8px 16px; font-size: 0.88rem; }

@media (max-width: 900px) {
.lb-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
.lbp-band { padding: 28px 0 72px; }
.lb-card { padding: 20px 18px; border-radius: 18px; }
.lb-grid { grid-template-columns: minmax(0, 1fr); }
.lb-submit { width: 100%; }
}
.lb .pp-submit:disabled { background: var(--accent); color: #fff; opacity: 0.4; box-shadow: none; }
55 changes: 55 additions & 0 deletions apps/web/src/app/project.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* A customer's project (/[locale]/account/[id]) in the look of the account page: a tinted head,
pill tabs, the panels as white cards with a shadow, the change chat with the prototype form's
inputs. ProjectDetail and ChangeChat use shared classes, so everything is scoped under `.pj`. */

.pj { background: linear-gradient(180deg, var(--accent-soft) 0, rgba(238, 242, 255, 0) 320px); padding: 36px 0 80px; min-height: 70vh; }
.pj .wrap { max-width: 1200px; }

/* Head */
.pj .detail-head { align-items: center; gap: 12px 14px; margin-top: 6px; }
.pj .detail-head h1 { font-size: clamp(2rem, 3.6vw, 2.8rem); letter-spacing: -0.04em; margin: 0; }
.pj .detail-head .acc-pill { font-size: 0.8rem; padding: 5px 12px; }
.pj > .wrap > div > p:first-child a { font-size: 0.88rem; font-weight: 600; color: var(--ink-soft); }
.pj > .wrap > div > p:first-child a:hover { color: var(--accent); }

/* Pill tabs instead of an underline */
.pj .tabs { border-bottom: 0; gap: 6px; margin: 24px 0 22px; padding: 4px; background: #fff; border: 1px solid var(--border); border-radius: 99px; width: fit-content; max-width: 100%; box-shadow: var(--shadow); }
.pj .tab { border: 0; margin: 0; border-radius: 99px; padding: 8px 18px; font-size: 0.9rem; }
.pj .tab[aria-selected="true"] { background: var(--accent); color: #fff; border: 0; }
.pj .tab:focus-visible { outline-offset: 2px; }

/* Panels */
.pj .card { border-radius: 18px; padding: 22px 24px; box-shadow: var(--shadow); gap: 10px; }
.pj .card h3 { font-size: 1.1rem; margin: 0 0 4px; }
.pj .detail-stack { gap: 16px; max-width: none; }
.pj .detail-layout { gap: 28px; }
.pj .note { border-radius: 12px; max-width: none; }
.pj .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.pj .btn-ghost { background: #fff; border: 1px solid var(--border); color: var(--ink); }
.pj .btn-ghost:hover { border-color: #c7d2fe; color: var(--accent-hover); }
.pj .btn-block { width: 100%; }
.pj .btn:disabled, .pj .btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
.pj .lang-toggle { cursor: pointer; }
.pj .tbl-wrap { border-radius: 14px; }
.pj .choice { border-radius: 12px; }
.pj .choice input { accent-color: var(--accent); }
.pj .choice:has(input:checked) { background: var(--accent-soft); }

/* Change chat */
.pj .chat-compose textarea { border-color: #d5dae4; border-radius: 12px; padding: 12px 14px; font-size: 0.95rem; background: #fff; }
.pj .chat-compose textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(29, 78, 216, 0.12); }
.pj .chat-attach { border-radius: 12px; background: #fff; color: var(--accent); }
.pj .chat-attach:hover { border-color: #c7d2fe; }
.chat-attach-ico { width: 20px; height: 20px; }
.pj .chat-bubble { border-radius: 16px; }
.pj .chat-option { border-radius: 99px; }

/* Phone preview */
.pj .phone-actions .lang-toggle { background: #fff; }

@media (max-width: 640px) {
.pj { padding-top: 24px; }
.pj .tabs { width: 100%; }
.pj .tab { flex: 1; padding: 8px 10px; }
.pj .card { padding: 18px; }
}
30 changes: 15 additions & 15 deletions apps/web/src/components/CampaignsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,26 +74,26 @@ export function CampaignsPanel({ d, token }: { d: Dict; token: string }) {
})[s];

return (
<section style={{ marginTop: 48 }}>
<h2 style={{ fontSize: "1.2rem" }}>{a.campaignsTitle}</h2>
<p className="est-empty">{a.campaignsIntro}</p>
<p className="est-empty" style={{ fontSize: ".85rem" }}>
<section className="lb-card lb-camps">
<h2>{a.campaignsTitle}</h2>
<p className="lb-note">{a.campaignsIntro}</p>
<p className="lb-platforms">
{(["meta", "google"] as const).map((p) => (
<span key={p} style={{ marginRight: 16 }}>
<span key={p} className={platforms[p] ? "is-on" : undefined}>
{p}: {platforms[p] ? "✓" : a.platformOff}
</span>
))}
</p>

{campaigns.length === 0 ? (
<p className="est-empty">{a.noCampaigns}</p>
<p className="lb-empty">{a.noCampaigns}</p>
) : (
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
<ul className="lb-camp-list">
{campaigns.map((c) => {
const ready = platforms[c.platform] && c.problems.length === 0;
return (
<li key={c.id} style={{ padding: "14px 0", borderTop: "1px solid rgba(255,255,255,.12)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 16, alignItems: "baseline" }}>
<li key={c.id}>
<div className="lb-camp-head">
<strong>
{c.platform} · {c.project.name.slice(0, 28)}
</strong>
Expand All @@ -102,29 +102,29 @@ export function CampaignsPanel({ d, token }: { d: Dict; token: string }) {
</small>
</div>

{c.error && <p className="est-empty" style={{ color: "#f87171" }}>{c.error}</p>}
{c.error && <p className="pp-error">{c.error}</p>}

{c.problems.length > 0 && (
<ul style={{ fontSize: ".82rem", color: "#fbbf24", margin: "6px 0", paddingLeft: 18 }}>
<ul className="lb-problems">
{c.problems.slice(0, 4).map((p, i) => (
<li key={i}>{p}</li>
))}
</ul>
)}

<div style={{ display: "flex", gap: 10, marginTop: 8, flexWrap: "wrap" }}>
<div className="lb-camp-actions">
{["unpublished", "failed"].includes(c.platform_status) && (
<button type="button" disabled={!ready || busy === c.id} onClick={() => act(c, "publish")}>
<button type="button" className="btn lb-ghost" disabled={!ready || busy === c.id} onClick={() => act(c, "publish")}>
{a.publish}
</button>
)}
{c.platform_status === "paused" && (
<button type="button" disabled={busy === c.id} onClick={() => act(c, "activate")}>
<button type="button" className="btn btn-primary" disabled={busy === c.id} onClick={() => act(c, "activate")}>
{a.activate}
</button>
)}
{c.platform_status === "active" && (
<button type="button" disabled={busy === c.id} onClick={() => act(c, "pause")}>
<button type="button" className="btn lb-ghost" disabled={busy === c.id} onClick={() => act(c, "pause")}>
{a.pauseAd}
</button>
)}
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/ChangeChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { api, ApiError, API_BASE } from "@/lib/api";
import { eur, type Dict, type Locale } from "@/lib/i18n";
import { Icon } from "./LineIcon";

/** One line of the thread, as GET /me/projects/{id}/messages returns it. */
export interface ChatMessage {
Expand Down Expand Up @@ -358,7 +359,7 @@ export function ChangeChat({
/>
<div className="chat-compose-side">
<label className="chat-attach" title={t.attach}>
<span aria-hidden>📎</span>
<Icon name="image" className="chat-attach-ico" />
<span className="sr-only">{t.attach}</span>
<input
type="file"
Expand Down
Loading
Loading