From 8a2b60bbb2de7a05b093afa4473d976e3703183f Mon Sep 17 00:00:00 2001 From: codemenschendev Date: Sat, 26 Sep 2026 15:48:57 +0700 Subject: [PATCH] design(web): project page and ads lab in the home page look The last two signed-in pages still had the old style. Project page (/account/[id]): - Tinted head, the status as the account page's pill in words ("fertig", "in Arbeit") instead of the raw value READY. - Pill tabs, panels as cards with a shadow, the change chat with the prototype form's inputs, a picture icon instead of the paperclip emoji. Ads lab (/lab): - Head band with the intro, the generator form in a card with labelled, styled selects in a three-column grid. - Rendered ads as a card list with a kind icon; campaigns in a card with platform pills and problems on the warning tint. The old yellow and red text came from a dark theme and was hard to read on white. Also: the account page moves to the 1200px frame, so it lines up with the header after the header change in #149. Project: Appwerk --- .../app/(site)/[locale]/account/[id]/page.tsx | 9 +- apps/web/src/app/(site)/[locale]/lab/page.tsx | 15 ++- apps/web/src/app/account.css | 1 + apps/web/src/app/lab.css | 63 +++++++++++++ apps/web/src/app/project.css | 55 +++++++++++ apps/web/src/components/CampaignsPanel.tsx | 30 +++--- apps/web/src/components/ChangeChat.tsx | 3 +- apps/web/src/components/LabPanel.tsx | 92 +++++++++---------- apps/web/src/components/ProjectDetail.tsx | 6 +- 9 files changed, 201 insertions(+), 73 deletions(-) create mode 100644 apps/web/src/app/lab.css create mode 100644 apps/web/src/app/project.css diff --git a/apps/web/src/app/(site)/[locale]/account/[id]/page.tsx b/apps/web/src/app/(site)/[locale]/account/[id]/page.tsx index 67b5a16..0a420ab 100644 --- a/apps/web/src/app/(site)/[locale]/account/[id]/page.tsx +++ b/apps/web/src/app/(site)/[locale]/account/[id]/page.tsx @@ -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, @@ -13,8 +16,10 @@ export default async function ProjectPage({ const d = getDict(locale); return ( -
- +
+
+ +
); } diff --git a/apps/web/src/app/(site)/[locale]/lab/page.tsx b/apps/web/src/app/(site)/[locale]/lab/page.tsx index 9f1fb2a..d64ce4a 100644 --- a/apps/web/src/app/(site)/[locale]/lab/page.tsx +++ b/apps/web/src/app/(site)/[locale]/lab/page.tsx @@ -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. @@ -18,9 +20,16 @@ export default async function LabPage({ const d = getDict(locale); return ( -
-

{d.lab.title}

- +
+
+
+

{d.lab.title}

+

{d.lab.intro}

+
+
+
+ +
); } diff --git a/apps/web/src/app/account.css b/apps/web/src/app/account.css index 105f3f7..933f9e9 100644 --- a/apps/web/src/app/account.css +++ b/apps/web/src/app/account.css @@ -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; } diff --git a/apps/web/src/app/lab.css b/apps/web/src/app/lab.css new file mode 100644 index 0000000..ace3e89 --- /dev/null +++ b/apps/web/src/app/lab.css @@ -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; } diff --git a/apps/web/src/app/project.css b/apps/web/src/app/project.css new file mode 100644 index 0000000..3c4347c --- /dev/null +++ b/apps/web/src/app/project.css @@ -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; } +} diff --git a/apps/web/src/components/CampaignsPanel.tsx b/apps/web/src/components/CampaignsPanel.tsx index 59b8114..ed3bf12 100644 --- a/apps/web/src/components/CampaignsPanel.tsx +++ b/apps/web/src/components/CampaignsPanel.tsx @@ -74,26 +74,26 @@ export function CampaignsPanel({ d, token }: { d: Dict; token: string }) { })[s]; return ( -
-

{a.campaignsTitle}

-

{a.campaignsIntro}

-

+

+

{a.campaignsTitle}

+

{a.campaignsIntro}

+

{(["meta", "google"] as const).map((p) => ( - + {p}: {platforms[p] ? "✓" : a.platformOff} ))}

{campaigns.length === 0 ? ( -

{a.noCampaigns}

+

{a.noCampaigns}

) : ( -
    +
      {campaigns.map((c) => { const ready = platforms[c.platform] && c.problems.length === 0; return ( -
    • -
      +
    • +
      {c.platform} · {c.project.name.slice(0, 28)} @@ -102,29 +102,29 @@ export function CampaignsPanel({ d, token }: { d: Dict; token: string }) {
      - {c.error &&

      {c.error}

      } + {c.error &&

      {c.error}

      } {c.problems.length > 0 && ( -
        +
          {c.problems.slice(0, 4).map((p, i) => (
        • {p}
        • ))}
        )} -
        +
        {["unpublished", "failed"].includes(c.platform_status) && ( - )} {c.platform_status === "paused" && ( - )} {c.platform_status === "active" && ( - )} diff --git a/apps/web/src/components/ChangeChat.tsx b/apps/web/src/components/ChangeChat.tsx index 3cd00fb..8f7be02 100644 --- a/apps/web/src/components/ChangeChat.tsx +++ b/apps/web/src/components/ChangeChat.tsx @@ -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 { @@ -358,7 +359,7 @@ export function ChangeChat({ />