Initial Paprika Next.js implementation
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
DATABASE_URL="postgresql://paprika_app:REPLACE_WITH_SECURE_PASSWORD@127.0.0.1:5432/paprika"
|
||||
NEXT_PUBLIC_SITE_URL="https://paprikalaw.com"
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.next/
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.env
|
||||
.env*.local
|
||||
*.log
|
||||
tsconfig.tsbuildinfo
|
||||
git.txt
|
||||
31
app/[page]/page.tsx
Normal file
31
app/[page]/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { LegacyPage } from "@/components/LegacyPage";
|
||||
import { legacyPages, type LegacyPageKey } from "@/lib/legacy-pages";
|
||||
|
||||
const routablePages = new Set(Object.keys(legacyPages));
|
||||
const replacedPages = new Set(["home", "start", "ask", "thanks"]);
|
||||
|
||||
export function generateStaticParams() {
|
||||
return Object.keys(legacyPages)
|
||||
.filter((page) => !replacedPages.has(page))
|
||||
.map((page) => ({ page }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ page: string }> }) {
|
||||
const { page: pageParam } = await params;
|
||||
if (!routablePages.has(pageParam)) return {};
|
||||
const page = legacyPages[pageParam as LegacyPageKey];
|
||||
return {
|
||||
title: page.title.replace("Paprika - ", "")
|
||||
};
|
||||
}
|
||||
|
||||
export default async function StaticPage({ params }: { params: Promise<{ page: string }> }) {
|
||||
const { page } = await params;
|
||||
|
||||
if (!routablePages.has(page) || replacedPages.has(page)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <LegacyPage pageKey={page as LegacyPageKey} />;
|
||||
}
|
||||
33
app/api/submissions/route.ts
Normal file
33
app/api/submissions/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { ZodError } from "zod";
|
||||
import { createSubmission } from "@/lib/submissions";
|
||||
import { submissionSchema } from "@/lib/submission-schema";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await request.json();
|
||||
const input = submissionSchema.parse(payload);
|
||||
const submission = await createSubmission(input);
|
||||
|
||||
return NextResponse.json({
|
||||
submission_id: submission.id,
|
||||
created_at: submission.createdAt.toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Validation failed",
|
||||
issues: error.flatten().fieldErrors
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.error("Submission failed", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Submission could not be saved. Please try again." },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
29
app/ask/page.tsx
Normal file
29
app/ask/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Link from "next/link";
|
||||
import { SubmissionForm } from "@/components/forms/SubmissionForm";
|
||||
|
||||
export const metadata = {
|
||||
title: "Ask a Question"
|
||||
};
|
||||
|
||||
export default function AskPage() {
|
||||
return (
|
||||
<>
|
||||
<section className="hero subpage press-hero">
|
||||
<h1>Ask a question.</h1>
|
||||
<p className="subhead">
|
||||
Not ready to submit your full profile yet? Send us a quick question. We will help you
|
||||
understand whether Paprika may be a fit.
|
||||
</p>
|
||||
<div className="actions">
|
||||
<Link className="button primary" href="/start">
|
||||
Start Here
|
||||
</Link>
|
||||
<Link className="button secondary" href="/ask">
|
||||
Ask a Question
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<SubmissionForm type="ask" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
2263
app/globals.css
Normal file
2263
app/globals.css
Normal file
File diff suppressed because it is too large
Load Diff
44
app/layout.tsx
Normal file
44
app/layout.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { SiteHeader } from "@/components/SiteHeader";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || "https://paprikalaw.com"),
|
||||
title: {
|
||||
default: "Paprika",
|
||||
template: "Paprika - %s"
|
||||
},
|
||||
description:
|
||||
"Attorney-curated evidence building for O-1 talent through press, judging opportunities, scholarly articles, and documented profile strategy.",
|
||||
openGraph: {
|
||||
title: "Paprika",
|
||||
description:
|
||||
"Attorney-curated evidence building for O-1 talent through press, judging opportunities, scholarly articles, and documented profile strategy.",
|
||||
url: "/",
|
||||
siteName: "Paprika",
|
||||
type: "website"
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<SiteHeader />
|
||||
<main>{children}</main>
|
||||
<footer>
|
||||
<div className="footer-links">
|
||||
<Link href="/why">About</Link>
|
||||
<Link href="/faq">FAQ</Link>
|
||||
<Link href="/terms">Terms</Link>
|
||||
<Link href="/privacy">Privacy</Link>
|
||||
<Link href="/disclaimer">Disclaimer</Link>
|
||||
</div>
|
||||
<div>Paprika is not a law firm and does not provide legal advice or immigration representation.</div>
|
||||
<div>Use of our services does not create an attorney-client relationship.</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
5
app/page.tsx
Normal file
5
app/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LegacyPage } from "@/components/LegacyPage";
|
||||
|
||||
export default function HomePage() {
|
||||
return <LegacyPage pageKey="home" />;
|
||||
}
|
||||
13
app/robots.ts
Normal file
13
app/robots.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://paprikalaw.com";
|
||||
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/"
|
||||
},
|
||||
sitemap: `${siteUrl}/sitemap.xml`
|
||||
};
|
||||
}
|
||||
28
app/sitemap.ts
Normal file
28
app/sitemap.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const routeOrder = [
|
||||
"home",
|
||||
"press",
|
||||
"judging",
|
||||
"scholarly",
|
||||
"services",
|
||||
"why",
|
||||
"resources",
|
||||
"faq",
|
||||
"start",
|
||||
"ask",
|
||||
"terms",
|
||||
"privacy",
|
||||
"disclaimer"
|
||||
] as const;
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://paprikalaw.com";
|
||||
|
||||
return routeOrder.map((key) => ({
|
||||
url: `${siteUrl}${key === "home" ? "" : `/${key}`}`,
|
||||
lastModified: new Date("2026-07-06"),
|
||||
changeFrequency: key === "home" ? "weekly" : "monthly",
|
||||
priority: key === "home" ? 1 : key === "start" ? 0.9 : 0.7
|
||||
}));
|
||||
}
|
||||
30
app/start/page.tsx
Normal file
30
app/start/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import Link from "next/link";
|
||||
import { SubmissionForm } from "@/components/forms/SubmissionForm";
|
||||
|
||||
export const metadata = {
|
||||
title: "Start Here"
|
||||
};
|
||||
|
||||
export default function StartPage() {
|
||||
return (
|
||||
<>
|
||||
<section className="hero subpage press-hero">
|
||||
<h1>Start here.</h1>
|
||||
<p className="subhead">
|
||||
Tell us a little about your background, goals, and timeline, whether you are actively
|
||||
preparing or just starting to explore. No need to have everything figured out. A quick
|
||||
summary is enough to start.
|
||||
</p>
|
||||
<div className="actions">
|
||||
<Link className="button primary" href="/start">
|
||||
Start Here
|
||||
</Link>
|
||||
<Link className="button secondary" href="/ask">
|
||||
Ask a Question
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<SubmissionForm type="start" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
29
app/thanks/page.tsx
Normal file
29
app/thanks/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata = {
|
||||
title: "Thank You"
|
||||
};
|
||||
|
||||
export default function ThanksPage({
|
||||
searchParams
|
||||
}: {
|
||||
searchParams: { submission_id?: string };
|
||||
}) {
|
||||
return (
|
||||
<section className="notice">
|
||||
<h2>Thank you. We received your background.</h2>
|
||||
<p>
|
||||
The Paprika team will review your profile and follow up with next steps. If your timeline is
|
||||
urgent, mention that in your reply.
|
||||
</p>
|
||||
{searchParams.submission_id ? (
|
||||
<p className="form-success">Submission ID: {searchParams.submission_id}</p>
|
||||
) : null}
|
||||
<div className="actions">
|
||||
<Link className="button secondary" href="/">
|
||||
Back to Homepage
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
8
components/LegacyPage.tsx
Normal file
8
components/LegacyPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { LegacyPageKey } from "@/lib/legacy-pages";
|
||||
import { legacyPages } from "@/lib/legacy-pages";
|
||||
|
||||
export function LegacyPage({ pageKey }: { pageKey: LegacyPageKey }) {
|
||||
const page = legacyPages[pageKey];
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: page.html }} />;
|
||||
}
|
||||
56
components/SiteHeader.tsx
Normal file
56
components/SiteHeader.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { navItems, resourceItems } from "@/lib/navigation";
|
||||
|
||||
export function SiteHeader() {
|
||||
const pathname = usePathname();
|
||||
const current = pathname === "/" ? "home" : pathname.split("/")[1];
|
||||
const resourcesActive = current === "faq" || current === "resources";
|
||||
|
||||
return (
|
||||
<header className="site-header">
|
||||
<Link className="brand" href="/">
|
||||
<div className="brand-name">Paprika</div>
|
||||
<div className="brand-tag">Attorney Curated Evidence Building for O-1 Talent</div>
|
||||
</Link>
|
||||
<nav className="nav" id="nav">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
data-page={item.key}
|
||||
className={current === item.key ? "active" : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
<div className="nav-dropdown">
|
||||
<Link
|
||||
href="/resources"
|
||||
className={`nav-trigger${resourcesActive ? " active" : ""}`}
|
||||
data-page="resources"
|
||||
>
|
||||
Resources <span className="nav-chevron">⌄</span>
|
||||
</Link>
|
||||
<div className="resources-menu" aria-label="Resources menu">
|
||||
{resourceItems.map((item) => (
|
||||
<Link key={item.key} href={item.href} data-page={item.key}>
|
||||
<strong>{item.label}</strong>
|
||||
<span>{item.description}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href="/start"
|
||||
className={`start${current === "start" ? " active" : ""}`}
|
||||
data-page="start"
|
||||
>
|
||||
Start Here <span className="button-arrow">→</span>
|
||||
</Link>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
150
components/forms/SubmissionForm.tsx
Normal file
150
components/forms/SubmissionForm.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, type FormEvent } from "react";
|
||||
|
||||
type FormType = "start" | "ask";
|
||||
|
||||
type FieldErrors = Record<string, string[] | undefined>;
|
||||
|
||||
export function SubmissionForm({ type }: { type: FormType }) {
|
||||
const router = useRouter();
|
||||
const [errors, setErrors] = useState<FieldErrors>({});
|
||||
const [status, setStatus] = useState<"idle" | "submitting" | "error">("idle");
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setErrors({});
|
||||
setStatus("submitting");
|
||||
|
||||
const form = new FormData(event.currentTarget);
|
||||
const payload = Object.fromEntries(form.entries());
|
||||
|
||||
const response = await fetch("/api/submissions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
type,
|
||||
consentAccepted: form.get("consentAccepted") === "on",
|
||||
sourcePath: type === "start" ? "/start" : "/ask"
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json().catch(() => ({}));
|
||||
|
||||
if (!response.ok) {
|
||||
setErrors(result.issues || { form: [result.error || "Submission failed"] });
|
||||
setStatus("error");
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/thanks?submission_id=${encodeURIComponent(result.submission_id)}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="form-wrap" onSubmit={handleSubmit}>
|
||||
<label className="field">
|
||||
Name <span className="req">*</span>
|
||||
<input className="field-control" name="name" autoComplete="name" required />
|
||||
<FieldError errors={errors} name="name" />
|
||||
</label>
|
||||
<label className="field">
|
||||
Email <span className="req">*</span>
|
||||
<input className="field-control" name="email" type="email" autoComplete="email" required />
|
||||
<FieldError errors={errors} name="email" />
|
||||
</label>
|
||||
<label className="field">
|
||||
LinkedIn {type === "ask" ? <span>(optional)</span> : null}
|
||||
<input className="field-control" name="linkedin" type="url" placeholder="https://linkedin.com/in/..." />
|
||||
<FieldError errors={errors} name="linkedin" />
|
||||
</label>
|
||||
|
||||
{type === "start" ? <StartFields errors={errors} /> : <AskFields errors={errors} />}
|
||||
|
||||
<label className="field large checkbox-field">
|
||||
<input name="consentAccepted" type="checkbox" required />
|
||||
<span>
|
||||
I understand that submitting this form does not create an attorney-client relationship or
|
||||
constitute legal advice. Paprika is not a law firm and does not provide legal services.
|
||||
</span>
|
||||
<FieldError errors={errors} name="consentAccepted" />
|
||||
</label>
|
||||
|
||||
{errors.form?.length ? <div className="form-error">{errors.form[0]}</div> : null}
|
||||
|
||||
<button className="button primary" type="submit" disabled={status === "submitting"}>
|
||||
{status === "submitting" ? "Submitting..." : type === "start" ? "Submit Profile" : "Send Question"}
|
||||
</button>
|
||||
<div className="form-note">
|
||||
File upload is intentionally disabled until storage, size limits, access control, and privacy
|
||||
handling are approved.
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function StartFields({ errors }: { errors: FieldErrors }) {
|
||||
return (
|
||||
<>
|
||||
<label className="field">
|
||||
Company Website
|
||||
<input className="field-control" name="companyWebsite" type="url" placeholder="https://..." />
|
||||
<FieldError errors={errors} name="companyWebsite" />
|
||||
</label>
|
||||
<label className="field">
|
||||
Current Role / Company
|
||||
<input className="field-control" name="currentRoleCompany" />
|
||||
</label>
|
||||
<label className="field">
|
||||
Field or Industry
|
||||
<input className="field-control" name="fieldOrIndustry" />
|
||||
</label>
|
||||
<label className="field large start-question-field">
|
||||
<strong>
|
||||
What brings you to Paprika? <span className="req">*</span>
|
||||
</strong>
|
||||
<span>
|
||||
Tell us where you are in your O-1 journey. Whether you have a specific goal, are building
|
||||
evidence early, or are just exploring, a quick summary is enough.
|
||||
</span>
|
||||
<textarea className="field-control" name="reason" required />
|
||||
<FieldError errors={errors} name="reason" />
|
||||
</label>
|
||||
<label className="field large start-question-field">
|
||||
<strong>What recognition have you already built, if any?</strong>
|
||||
<span>
|
||||
Press, interviews, judging roles, awards, speaking, scholarly articles, patents, funding,
|
||||
major projects, or other recognition.
|
||||
</span>
|
||||
<textarea className="field-control" name="existingRecognition" />
|
||||
</label>
|
||||
<label className="field large start-question-field">
|
||||
<strong>Is there a timeline on your mind?</strong>
|
||||
<span>
|
||||
Whether it is urgent, months away, or just a future possibility, an approximate timeline
|
||||
helps us recommend the right pace.
|
||||
</span>
|
||||
<textarea className="field-control" name="timeline" />
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AskFields({ errors }: { errors: FieldErrors }) {
|
||||
return (
|
||||
<label className="field large question-field">
|
||||
<strong>
|
||||
Your question <span className="req">*</span>
|
||||
</strong>
|
||||
<span>Ask us anything about our services, your situation, or where to start.</span>
|
||||
<textarea className="field-control" name="question" required />
|
||||
<FieldError errors={errors} name="question" />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldError({ errors, name }: { errors: FieldErrors; name: string }) {
|
||||
const message = errors[name]?.[0];
|
||||
return message ? <span className="form-error">{message}</span> : null;
|
||||
}
|
||||
27
doc/README.md
Normal file
27
doc/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Paprika Frontend Mockup
|
||||
|
||||
This repository contains the current Paprika website mockup.
|
||||
|
||||
## Open the mockup
|
||||
|
||||
Open `index.html` in a browser.
|
||||
|
||||
The mockup is a single self-contained HTML file with all page routes handled through query parameters, including:
|
||||
|
||||
- `?page=home`
|
||||
- `?page=press`
|
||||
- `?page=judging`
|
||||
- `?page=scholarly`
|
||||
- `?page=services`
|
||||
- `?page=why`
|
||||
- `?page=resources`
|
||||
- `?page=faq`
|
||||
- `?page=terms`
|
||||
- `?page=privacy`
|
||||
- `?page=disclaimer`
|
||||
- `?page=start`
|
||||
- `?page=ask`
|
||||
|
||||
## Upload rule
|
||||
|
||||
After each approved refinement is completed, commit the updated mockup and upload it to the cloud repository immediately.
|
||||
3423
doc/index.html
Normal file
3423
doc/index.html
Normal file
File diff suppressed because it is too large
Load Diff
10
eslint.config.mjs
Normal file
10
eslint.config.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
|
||||
const config = [
|
||||
...nextVitals,
|
||||
{
|
||||
ignores: [".next/**", "node_modules/**", "lib/legacy-pages.ts"]
|
||||
}
|
||||
];
|
||||
|
||||
export default config;
|
||||
15
lib/db.ts
Normal file
15
lib/db.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma?: PrismaClient;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"]
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
83
lib/legacy-pages.ts
Normal file
83
lib/legacy-pages.ts
Normal file
File diff suppressed because one or more lines are too long
22
lib/navigation.ts
Normal file
22
lib/navigation.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const navItems = [
|
||||
{ href: "/press", label: "Press", key: "press" },
|
||||
{ href: "/judging", label: "Judging", key: "judging" },
|
||||
{ href: "/scholarly", label: "Scholarly Articles", key: "scholarly" },
|
||||
{ href: "/services", label: "Services", key: "services" },
|
||||
{ href: "/why", label: "Why Paprika", key: "why" }
|
||||
] as const;
|
||||
|
||||
export const resourceItems = [
|
||||
{
|
||||
href: "/faq",
|
||||
key: "faq",
|
||||
label: "Frequently Asked Questions",
|
||||
description: "Common questions before you start"
|
||||
},
|
||||
{
|
||||
href: "/resources",
|
||||
key: "resources",
|
||||
label: "O-1A Criteria Overview",
|
||||
description: "All 8 O-1A criteria, explained"
|
||||
}
|
||||
] as const;
|
||||
46
lib/submission-schema.ts
Normal file
46
lib/submission-schema.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const emptyToUndefined = (value: unknown) => {
|
||||
if (typeof value !== "string") return value;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length ? trimmed : undefined;
|
||||
};
|
||||
|
||||
const optionalString = z.preprocess(emptyToUndefined, z.string().max(4000).optional());
|
||||
const optionalUrl = z.preprocess(
|
||||
emptyToUndefined,
|
||||
z.string().url("Enter a valid URL, including https://").max(500).optional()
|
||||
);
|
||||
|
||||
const baseSubmissionSchema = z.object({
|
||||
type: z.enum(["start", "ask"]),
|
||||
name: z.string().trim().min(1, "Name is required").max(160),
|
||||
email: z.string().trim().email("Enter a valid email").max(255),
|
||||
linkedin: optionalUrl,
|
||||
consentAccepted: z.literal(true, {
|
||||
errorMap: () => ({ message: "You must acknowledge the disclaimer before submitting" })
|
||||
}),
|
||||
sourcePath: z.string().max(200).optional()
|
||||
});
|
||||
|
||||
export const startSubmissionSchema = baseSubmissionSchema.extend({
|
||||
type: z.literal("start"),
|
||||
companyWebsite: optionalUrl,
|
||||
currentRoleCompany: optionalString,
|
||||
fieldOrIndustry: optionalString,
|
||||
reason: z.string().trim().min(1, "Tell us what brings you to Paprika").max(4000),
|
||||
existingRecognition: optionalString,
|
||||
timeline: optionalString
|
||||
});
|
||||
|
||||
export const askSubmissionSchema = baseSubmissionSchema.extend({
|
||||
type: z.literal("ask"),
|
||||
question: z.string().trim().min(1, "Question is required").max(4000)
|
||||
});
|
||||
|
||||
export const submissionSchema = z.discriminatedUnion("type", [
|
||||
startSubmissionSchema,
|
||||
askSubmissionSchema
|
||||
]);
|
||||
|
||||
export type SubmissionInput = z.infer<typeof submissionSchema>;
|
||||
26
lib/submissions.ts
Normal file
26
lib/submissions.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import type { SubmissionInput } from "@/lib/submission-schema";
|
||||
|
||||
export async function createSubmission(input: SubmissionInput) {
|
||||
return prisma.submission.create({
|
||||
data: {
|
||||
type: input.type,
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
linkedin: input.linkedin,
|
||||
companyWebsite: input.type === "start" ? input.companyWebsite : undefined,
|
||||
currentRoleCompany: input.type === "start" ? input.currentRoleCompany : undefined,
|
||||
fieldOrIndustry: input.type === "start" ? input.fieldOrIndustry : undefined,
|
||||
reason: input.type === "start" ? input.reason : undefined,
|
||||
existingRecognition: input.type === "start" ? input.existingRecognition : undefined,
|
||||
timeline: input.type === "start" ? input.timeline : undefined,
|
||||
question: input.type === "ask" ? input.question : undefined,
|
||||
consentAccepted: input.consentAccepted,
|
||||
sourcePath: input.sourcePath
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
}
|
||||
6
next-env.d.ts
vendored
Normal file
6
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
4
next.config.mjs
Normal file
4
next.config.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
5783
package-lock.json
generated
Normal file
5783
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "meetpaprika",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
"generate:legacy": "node scripts/extract-legacy-pages.mjs",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.22.0",
|
||||
"next": "^16.2.10",
|
||||
"prisma": "^5.22.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "^16.2.10",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
39
prisma/migrations/20260706120000_init/migration.sql
Normal file
39
prisma/migrations/20260706120000_init/migration.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Submission" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"linkedin" TEXT,
|
||||
"companyWebsite" TEXT,
|
||||
"currentRoleCompany" TEXT,
|
||||
"fieldOrIndustry" TEXT,
|
||||
"reason" TEXT,
|
||||
"existingRecognition" TEXT,
|
||||
"timeline" TEXT,
|
||||
"question" TEXT,
|
||||
"consentAccepted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"sourcePath" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'new',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Submission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SubmissionFile" (
|
||||
"id" TEXT NOT NULL,
|
||||
"submissionId" TEXT NOT NULL,
|
||||
"fileName" TEXT NOT NULL,
|
||||
"mimeType" TEXT NOT NULL,
|
||||
"byteSize" INTEGER NOT NULL,
|
||||
"storageKey" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "SubmissionFile_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SubmissionFile" ADD CONSTRAINT "SubmissionFile_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "Submission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
40
prisma/schema.prisma
Normal file
40
prisma/schema.prisma
Normal file
@@ -0,0 +1,40 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Submission {
|
||||
id String @id @default(cuid())
|
||||
type String
|
||||
name String
|
||||
email String
|
||||
linkedin String?
|
||||
companyWebsite String?
|
||||
currentRoleCompany String?
|
||||
fieldOrIndustry String?
|
||||
reason String?
|
||||
existingRecognition String?
|
||||
timeline String?
|
||||
question String?
|
||||
consentAccepted Boolean @default(false)
|
||||
sourcePath String?
|
||||
status String @default("new")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
files SubmissionFile[]
|
||||
}
|
||||
|
||||
model SubmissionFile {
|
||||
id String @id @default(cuid())
|
||||
submissionId String
|
||||
fileName String
|
||||
mimeType String
|
||||
byteSize Int
|
||||
storageKey String
|
||||
createdAt DateTime @default(now())
|
||||
submission Submission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
101
scripts/extract-legacy-pages.mjs
Normal file
101
scripts/extract-legacy-pages.mjs
Normal file
@@ -0,0 +1,101 @@
|
||||
import fs from "node:fs";
|
||||
import vm from "node:vm";
|
||||
|
||||
const sourcePath = "doc/index.html";
|
||||
const html = fs.readFileSync(sourcePath, "utf8");
|
||||
|
||||
const styleMatch = html.match(/<style>([\s\S]*?)<\/style>/);
|
||||
const scriptMatch = html.match(/<script>([\s\S]*?)<\/script>/);
|
||||
|
||||
if (!styleMatch || !scriptMatch) {
|
||||
throw new Error("Could not find inline style or script in doc/index.html");
|
||||
}
|
||||
|
||||
const css = styleMatch[1]
|
||||
.replaceAll("href=\"?page=", "href=\"/")
|
||||
.replace(/href="\/home"/g, "href=\"/\"");
|
||||
|
||||
const legacyScript = scriptMatch[1];
|
||||
const renderScript = legacyScript.replace(
|
||||
/const params = new URLSearchParams\(window\.location\.search\);[\s\S]*$/,
|
||||
"globalThis.__pageData = pageData;"
|
||||
);
|
||||
|
||||
const sandbox = {
|
||||
console,
|
||||
URLSearchParams,
|
||||
window: { location: { search: "" } },
|
||||
document: {
|
||||
title: "",
|
||||
getElementById() {
|
||||
return { innerHTML: "" };
|
||||
},
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
},
|
||||
querySelector() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(renderScript, sandbox, { filename: sourcePath });
|
||||
|
||||
const pageData = sandbox.__pageData;
|
||||
const routeMap = {
|
||||
home: "/",
|
||||
press: "/press",
|
||||
judging: "/judging",
|
||||
scholarly: "/scholarly",
|
||||
services: "/services",
|
||||
why: "/why",
|
||||
resources: "/resources",
|
||||
faq: "/faq",
|
||||
thanks: "/thanks",
|
||||
terms: "/terms",
|
||||
privacy: "/privacy",
|
||||
disclaimer: "/disclaimer"
|
||||
};
|
||||
|
||||
const pages = Object.fromEntries(
|
||||
Object.entries(routeMap).map(([key, path]) => {
|
||||
const page = pageData[key];
|
||||
if (!page?.body) {
|
||||
throw new Error(`Missing page body for ${key}`);
|
||||
}
|
||||
return [
|
||||
key,
|
||||
{
|
||||
key,
|
||||
path,
|
||||
title: key === "home" ? "Paprika" : `Paprika - ${key}`,
|
||||
html: normalizeLinks(page.body)
|
||||
}
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
function normalizeLinks(value) {
|
||||
return value
|
||||
.replaceAll("href=\"?page=home\"", "href=\"/\"")
|
||||
.replace(/href="\?page=([a-z-]+)"/g, (_match, page) => {
|
||||
if (page === "home") return "href=\"/\"";
|
||||
return `href="/${page}"`;
|
||||
});
|
||||
}
|
||||
|
||||
fs.mkdirSync("app", { recursive: true });
|
||||
fs.mkdirSync("lib", { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
"app/globals.css",
|
||||
`${css}\n\n.form-error { color: var(--deep-red); font-size: 14px; font-weight: 750; line-height: 1.4; }\n.form-success { color: var(--green); font-size: 14px; font-weight: 750; line-height: 1.4; }\n.field-control { width: 100%; border: 0; outline: 0; background: transparent; color: var(--ink); font: inherit; font-weight: 650; }\n.field-control::placeholder { color: var(--muted); opacity: .78; }\ntextarea.field-control { min-height: 126px; resize: vertical; line-height: 1.45; }\n.checkbox-field { align-items: flex-start; gap: 12px; text-align: left; color: var(--text); font-size: 14px; line-height: 1.45; }\n.checkbox-field input { margin-top: 3px; accent-color: var(--red); }\n.button[disabled] { cursor: not-allowed; opacity: .62; }\n`
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
"lib/legacy-pages.ts",
|
||||
`export type LegacyPageKey = ${Object.keys(pages).map((key) => JSON.stringify(key)).join(" | ")};\n\nexport type LegacyPage = {\n key: LegacyPageKey;\n path: string;\n title: string;\n html: string;\n};\n\nexport const legacyPages = ${JSON.stringify(pages, null, 2)} as const satisfies Record<LegacyPageKey, LegacyPage>;\n`
|
||||
);
|
||||
|
||||
console.log(`Generated ${Object.keys(pages).length} pages from ${sourcePath}`);
|
||||
41
tsconfig.json
Normal file
41
tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
771
working/01-项目功能内容.md
Normal file
771
working/01-项目功能内容.md
Normal file
@@ -0,0 +1,771 @@
|
||||
# 01-项目功能内容
|
||||
|
||||
## 1. 项目基线
|
||||
|
||||
- 项目名称:MeetPaprika / Paprika
|
||||
- 当前对标文件:`/home/mes123456/MeetPaprika/doc/index.html`
|
||||
- 原始对标形态:单文件静态网站 mockup,HTML、CSS、页面数据、页面渲染脚本全部内联在 `doc/index.html`
|
||||
- 品牌主张:Attorney Curated Evidence Building for O-1 Talent
|
||||
- 业务定位:面向 O-1 人才的证据建设服务,帮助 founders、engineers、researchers、executives、creators 等提前建设媒体报道、评审机会、学术文章和完整证据包
|
||||
- 页面运行方式:读取 URL 查询参数 `?page=...`,将对应页面 HTML 字符串写入 `<main id="app"></main>`
|
||||
|
||||
## 2. 技术路线基准
|
||||
|
||||
### 当前静态实现
|
||||
|
||||
- 入口文件:`doc/index.html`
|
||||
- 样式:内联 `<style>`
|
||||
- 脚本:内联 `<script>`
|
||||
- 状态:作为旧版静态 mockup 和视觉对标基线保留;正式工程已在根目录新增
|
||||
- 路由:查询参数路由,不依赖 History API
|
||||
|
||||
### 已落地的正式开发技术架构
|
||||
|
||||
已明确采用:
|
||||
|
||||
- 前端框架:Next.js + React
|
||||
- 数据库:PostgreSQL
|
||||
- 数据访问层:Prisma ORM
|
||||
- 表单处理:Next.js Route Handler `POST /api/submissions` 承接 Start Here 和 Ask a Question
|
||||
- 部署端口:Next.js 服务或反向代理后的应用服务监听 `8082`
|
||||
- HTTPS:由 Nginx/Caddy/云服务反向代理终止 TLS,转发到本机 `8082`
|
||||
|
||||
架构目标:
|
||||
|
||||
- 将当前 `doc/index.html` 的视觉和内容完整迁移到 Next.js 页面/组件。
|
||||
- 将 `start` 和 `ask` 从视觉 mockup 改为真实表单。
|
||||
- 将提交数据保存到 PostgreSQL。
|
||||
- 预留 CV/resume 文件上传能力,但必须先明确存储方案、文件大小、访问权限和隐私规则。
|
||||
- 保留所有 legal disclaimer,防止用户误以为提交表单会创建 attorney-client relationship。
|
||||
|
||||
当前代码状态:
|
||||
|
||||
- 根目录已存在 Next.js App Router 工程。
|
||||
- `app/[page]/page.tsx` 渲染从 `doc/index.html` 抽取的静态页面内容。
|
||||
- `app/start/page.tsx` 和 `app/ask/page.tsx` 已替换为真实 React 表单。
|
||||
- `app/api/submissions/route.ts` 已实现 JSON 提交、Zod 校验和 Prisma 写入。
|
||||
- `prisma/schema.prisma` 和 `prisma/migrations/20260706120000_init/migration.sql` 已建立 `Submission`、`SubmissionFile` 表。
|
||||
- `.env.example` 已提供 `DATABASE_URL` 和 `NEXT_PUBLIC_SITE_URL` 模板,不包含真实密码。
|
||||
- 成功入库尚需生产或本地 PostgreSQL `DATABASE_URL`。
|
||||
|
||||
建议工程结构:
|
||||
|
||||
```text
|
||||
app/
|
||||
page.tsx
|
||||
press/page.tsx
|
||||
judging/page.tsx
|
||||
scholarly/page.tsx
|
||||
services/page.tsx
|
||||
why/page.tsx
|
||||
resources/page.tsx
|
||||
faq/page.tsx
|
||||
start/page.tsx
|
||||
ask/page.tsx
|
||||
thanks/page.tsx
|
||||
terms/page.tsx
|
||||
privacy/page.tsx
|
||||
disclaimer/page.tsx
|
||||
api/
|
||||
submissions/route.ts
|
||||
components/
|
||||
SiteHeader.tsx
|
||||
SiteFooter.tsx
|
||||
Hero.tsx
|
||||
ServiceCard.tsx
|
||||
ServicesCta.tsx
|
||||
LegalPage.tsx
|
||||
forms/
|
||||
StartForm.tsx
|
||||
AskForm.tsx
|
||||
lib/
|
||||
db.ts
|
||||
validators.ts
|
||||
submissions.ts
|
||||
prisma/
|
||||
schema.prisma
|
||||
```
|
||||
|
||||
### 目标发布环境
|
||||
|
||||
- 目标域名:`https://paprikalaw.com/`
|
||||
- 云服务器:`170.106.192.152`
|
||||
- 登录用户:`ubuntu`
|
||||
- 目标端口:`8082`
|
||||
- 可用端口段:`8080-8099`
|
||||
- PostgreSQL:用户确认已在服务器安装完成,待验证服务状态、版本、应用数据库和应用用户
|
||||
- SSH 凭据:用户已在交接信息中提供;长期文档不保存明文密码,后续应改为 SSH key、部署密钥或受控密钥库
|
||||
- 目标发布形态:HTTPS 对外访问,服务内部监听 `8082` 或由反向代理转发至 `8082`
|
||||
|
||||
### 邮箱系统需求
|
||||
|
||||
- 普通用户网页登录:`https://mail.paprikalaw.com/webmail/`
|
||||
- 管理员网页登录:`https://mail.paprikalaw.com/admin/`
|
||||
- 管理员邮箱:`admin@paprikalaw.com`
|
||||
- 普通公司邮箱:
|
||||
- `info@paprikalaw.com`
|
||||
- `contact@paprikalaw.com`
|
||||
- `support@paprikalaw.com`
|
||||
- `sales@paprikalaw.com`
|
||||
- `marketing@paprikalaw.com`
|
||||
- `hr@paprikalaw.com`
|
||||
- `finance@paprikalaw.com`
|
||||
- `billing@paprikalaw.com`
|
||||
- `legal@paprikalaw.com`
|
||||
- `service@paprikalaw.com`
|
||||
- 凭据规则:用户已提供统一初始密码;长期文档不保存明文密码。创建后应要求首次登录修改,或为每个邮箱生成独立强密码。
|
||||
|
||||
邮件系统与网站的关系:
|
||||
|
||||
- 表单通知邮箱建议使用 `support@paprikalaw.com` 或 `contact@paprikalaw.com`。
|
||||
- 法务和隐私页面中的联系邮箱目前是 `support@meetpaprika.com`,上线前需统一为 `paprikalaw.com` 域名邮箱或保留现状并记录理由。
|
||||
- 若 Next.js 表单提交后发送邮件通知,需要配置 SMTP 凭据、发件地址、收件地址和退信策略。
|
||||
|
||||
## 3. 当前页面路由清单
|
||||
|
||||
| 路由 | 页面 | 导航入口 | 当前用途 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `?page=home` | Home | 品牌 logo 默认入口 | 品牌主张、三大核心服务、提前建设证据、工作流程、价格起点 | 已实现 mockup |
|
||||
| `?page=press` | Press | 顶部导航 Press | 媒体报道服务、证据标准、应避免的 press 类型、代表性 outlet 网络 | 已实现 mockup |
|
||||
| `?page=judging` | Judging | 顶部导航 Judging | 评审机会服务、哪些 judging counts、judging network、证据包内容 | 已实现 mockup |
|
||||
| `?page=scholarly` | Scholarly Articles | 顶部导航 Scholarly Articles | 学术文章服务、发表策略、证据包价值 | 已实现 mockup |
|
||||
| `?page=services` | Services | 顶部导航 Services | 6 类服务卡片和服务转化 CTA | 已实现 mockup |
|
||||
| `?page=why` | Why Paprika | 顶部导航 Why Paprika、footer About | 差异化说明、证据质量、时间节奏、完整证据记录 | 已实现 mockup |
|
||||
| `?page=resources` | Resources / O-1A Criteria | Resources 下拉 | 8 项 O-1A criteria 的 plain-English overview | 已实现 mockup |
|
||||
| `?page=faq` | FAQ | Resources 下拉、footer FAQ | Paprika、O-1、timeline、pricing/refund FAQ | 已实现 mockup |
|
||||
| `?page=start` | Start Here | 顶部 CTA、各页面 CTA | 背景资料提交视觉 mockup | 已实现视觉,未实现提交 |
|
||||
| `?page=ask` | Ask a Question | 各页面 secondary CTA | 简短问题提交视觉 mockup | 已实现视觉,未实现提交 |
|
||||
| `?page=thanks` | Thank You | 提交按钮跳转 | 感谢页 | 已实现 mockup |
|
||||
| `?page=terms` | Terms | footer Terms | 服务条款草稿 | 已实现草稿,需法务确认 |
|
||||
| `?page=privacy` | Privacy | footer Privacy | 隐私政策草稿 | 已实现草稿,需法务确认 |
|
||||
| `?page=disclaimer` | Disclaimer | footer Disclaimer | 法律免责声明草稿 | 已实现草稿,需法务确认 |
|
||||
| `?page=blog` | Blog | 无公开导航 | Field notes 文章卡片占位 | 代码存在,未公开 |
|
||||
| `?page=about` | About | 无公开导航;footer About 实际指向 `why` | 公司说明占位 | 代码存在,未公开 |
|
||||
|
||||
未识别路由会回退到 `home`,页面标题为 `Paprika - ${selected}`。
|
||||
|
||||
## 4. 全站公共模块
|
||||
|
||||
### Header
|
||||
|
||||
- 品牌区:
|
||||
- `Paprika`
|
||||
- `Attorney Curated Evidence Building for O-1 Talent`
|
||||
- 顶部导航:
|
||||
- Press
|
||||
- Judging
|
||||
- Scholarly Articles
|
||||
- Services
|
||||
- Why Paprika
|
||||
- Resources 下拉
|
||||
- Start Here CTA
|
||||
- Resources 下拉:
|
||||
- Frequently Asked Questions -> `?page=faq`
|
||||
- O-1A Criteria Overview -> `?page=resources`
|
||||
- 当前页高亮:
|
||||
- 根据 `data-page` 和当前 `selected` 页面添加 `active`
|
||||
- 当 `selected` 为 `faq` 或 `resources` 时,Resources 触发器高亮
|
||||
|
||||
### Footer
|
||||
|
||||
- 链接:
|
||||
- About -> `?page=why`
|
||||
- FAQ -> `?page=faq`
|
||||
- Terms -> `?page=terms`
|
||||
- Privacy -> `?page=privacy`
|
||||
- Disclaimer -> `?page=disclaimer`
|
||||
- 固定声明:
|
||||
- Paprika is not a law firm and does not provide legal advice or immigration representation.
|
||||
- Use of our services does not create an attorney-client relationship.
|
||||
|
||||
## 5. 首页内容
|
||||
|
||||
### Hero
|
||||
|
||||
- H1:O-1 evidence should not be built in a panic.
|
||||
- Subhead:帮助高成就人才提前建设 credible public recognition
|
||||
- Primary CTA:Send Us Your Profile -> `?page=start`
|
||||
- Secondary CTA:Ask a Question -> `?page=ask`
|
||||
- Microcopy:提交 LinkedIn 或 CV 后 review background,并推荐 evidence path
|
||||
- 时间主张:The only thing you cannot buy later is time.
|
||||
|
||||
### Core services
|
||||
|
||||
三张服务卡片:
|
||||
|
||||
1. Press:Editorial feature in a recognized publication in your field.
|
||||
2. Judging:Opportunities to judge work that matters.
|
||||
3. Scholarly Articles:Scholarly articles in credible, peer-reviewed publications.
|
||||
|
||||
### Start Early
|
||||
|
||||
- 强调证据不能临近 filing 时集中出现
|
||||
- 目标是构建 natural publication and judging timeline
|
||||
- 输出给用户或 immigration attorney 使用的 stronger materials
|
||||
|
||||
### Our standard
|
||||
|
||||
提出推荐前要判断的 8 个问题:
|
||||
|
||||
1. 文章是否关于个人而不只是公司或产品
|
||||
2. 是否连接特定专业领域和 O-1 criteria
|
||||
3. outlet 是否 credible、independent、documentable
|
||||
4. timing 是否自然
|
||||
5. judging 是否真实 evaluative
|
||||
6. authorship 是否体现 expertise
|
||||
7. 是否能清晰整理进 evidence package
|
||||
8. 是否补强其他 existing criteria
|
||||
|
||||
### How it works
|
||||
|
||||
四步流程:
|
||||
|
||||
1. Send us your profile
|
||||
2. Assess background and recommend evidence path
|
||||
3. Build credible public record
|
||||
4. Receive documented evidence package
|
||||
|
||||
价格带:
|
||||
|
||||
- Packages start at `$1,699+`
|
||||
- final pricing depends on outlet level, timeline, field, evidence goals
|
||||
|
||||
## 6. Press 页面内容
|
||||
|
||||
### 核心主张
|
||||
|
||||
- Attorney-Curated Press for O-1 Talent
|
||||
- Press that works as evidence, not just visibility
|
||||
- 关注 outlet、format、field relevance、documentation
|
||||
|
||||
### Cadence note
|
||||
|
||||
- 避免 filing 前突然集中出现的 uncurated evidence
|
||||
- 构建 natural cadence of press over time
|
||||
|
||||
### What Paprika looks for
|
||||
|
||||
Press 推荐前检查:
|
||||
|
||||
1. 文章是否关于本人
|
||||
2. headline 是否出现姓名并连接领域
|
||||
3. 是否 genuine editorial coverage
|
||||
4. outlet 是否有真实 readership、editorial standards、clean track record
|
||||
5. 是否存在 contributor、sponsored、editorial-staff-not-involved 等 disclosure
|
||||
6. timing 是否自然
|
||||
7. 是否 complement other O-1 criteria
|
||||
|
||||
结论句:The outlet name is not the evidence. The editorial independence is.
|
||||
|
||||
### What to avoid
|
||||
|
||||
当前展示三类不推荐 press:
|
||||
|
||||
- Contributor content
|
||||
- Editorial staff not involved
|
||||
- Sponsored or promotional placement
|
||||
|
||||
原因:
|
||||
|
||||
- outlet 没有 editorially choose to cover this person
|
||||
- third party produced or paid for content
|
||||
- 不体现 independent recognition
|
||||
|
||||
### Representative outlets
|
||||
|
||||
页面列出代表性 outlet 网络,包括 Forbes、Bloomberg、Vogue、Business Insider、USA Today、TechCrunch、VentureBeat、Los Angeles Times、Newsweek、Fast Company Mexico、Time 等。正式上线前需要确认是否拥有真实合作网络、是否允许展示、是否需要增加免责声明。
|
||||
|
||||
## 7. Judging 页面内容
|
||||
|
||||
### 核心主张
|
||||
|
||||
- Judging roles should show that your field trusts your expertise.
|
||||
- 服务目标:source and vet selective evaluation opportunities
|
||||
|
||||
### What counts as judging
|
||||
|
||||
当前列出 6 类:
|
||||
|
||||
1. Startup pitch competitions
|
||||
2. Hackathon & innovation judging
|
||||
3. Industry award committees
|
||||
4. Academic & conference peer review
|
||||
5. Grant review panels
|
||||
6. Research competition judging
|
||||
|
||||
### Judging network examples
|
||||
|
||||
当前展示示例 chips:Y Combinator Demo Day Adjacent、TechCrunch Disrupt、SXSW Pitch、Web Summit、Slush、Collision、Startup Grind Global、MassChallenge、Plug and Play、500 Global、Berkeley SkyDeck、Techstars、Founder Institute、academic journal review、conference TPCs、design competition juries、arts award panels、research grant committees 等。
|
||||
|
||||
正式上线前需要确认名称使用风险、真实性、合作关系或改为“示例类型”。
|
||||
|
||||
### Evidence package
|
||||
|
||||
Judging package 当前包含:
|
||||
|
||||
- Signed organizer recommendation letter
|
||||
- Judge selection documentation
|
||||
- Organizer correspondence
|
||||
- Agenda & event page
|
||||
- Panel agenda & bio listing
|
||||
- Ready-to-use packet
|
||||
|
||||
## 8. Scholarly Articles 页面内容
|
||||
|
||||
核心内容:
|
||||
|
||||
- Scholarly articles in credible, peer-reviewed publications
|
||||
- 服务覆盖 peer-reviewed、Scopus-indexed、Google-verified journals
|
||||
- 两个 panel:
|
||||
- Attorney-curated strategy
|
||||
- Complete evidence packet
|
||||
|
||||
正式上线前需确认:
|
||||
|
||||
- 是否能承诺具体 indexing 或 journal 类型
|
||||
- 是否需要补充 publication ethics、authorship、conflict disclosure
|
||||
- 是否避免暗示 pay-to-publish 或不当 authorship arrangement
|
||||
|
||||
## 9. Services 页面内容
|
||||
|
||||
六张服务卡:
|
||||
|
||||
| 编号 | 服务 | 当前标题 | CTA |
|
||||
| --- | --- | --- | --- |
|
||||
| 01 | Press | Editorial feature in a recognized publication in your field. | Explore Press |
|
||||
| 02 | Judging | Opportunities to judge work that matters. | Explore Judging |
|
||||
| 03 | Scholarly Articles | Scholarly articles in credible, peer-reviewed publications. | Explore Articles |
|
||||
| 04 | Expert consultation | Know where you stand before you commit. | Start Here |
|
||||
| 05 | Agency representation | Useful when you have multiple engagements or no single employer. | Start Here |
|
||||
| 06 | Attorney Referral | The right attorney, when you need one. | Start Here |
|
||||
|
||||
注意:服务 04、05、06 曾经传入不存在的 `contact` 路由;R007 已改为 `start`。
|
||||
|
||||
## 10. Why Paprika 页面内容
|
||||
|
||||
五个差异化理由:
|
||||
|
||||
1. Attorney-curated. Built to matter.
|
||||
2. Built before you need it.
|
||||
3. A coherent profile, tailored to you.
|
||||
4. A complete evidence record.
|
||||
5. Everything in one place.
|
||||
|
||||
结尾 peace of mind:
|
||||
|
||||
- You do not have to second-guess what you are building.
|
||||
- You just build.
|
||||
|
||||
## 11. Resources / O-1A Criteria 页面内容
|
||||
|
||||
页面解释 O-1A 8 项 criteria:
|
||||
|
||||
1. Awards
|
||||
2. Memberships
|
||||
3. Published Material About You
|
||||
4. Judging the Work of Others
|
||||
5. Original Contributions of Major Significance
|
||||
6. Scholarly Articles
|
||||
7. Critical or Essential Role
|
||||
8. High Salary or Remuneration
|
||||
|
||||
每项都包含:
|
||||
|
||||
- subtitle
|
||||
- plain-English body
|
||||
- stronger examples
|
||||
- watch out for
|
||||
- bottom line
|
||||
|
||||
页面有 disclaimer:此 overview 是 general information,不是 legal advice,Paprika 不是 law firm。
|
||||
|
||||
## 12. FAQ 页面内容
|
||||
|
||||
FAQ 分组:
|
||||
|
||||
- About Paprika
|
||||
- O-1 Basics
|
||||
- Timeline
|
||||
- Pricing, scope, and refunds
|
||||
|
||||
关键问答覆盖:
|
||||
|
||||
- Paprika 是否 law firm
|
||||
- Paprika 做什么
|
||||
- 适合谁
|
||||
- process 如何工作
|
||||
- 何时开始
|
||||
- 是否 guarantee visa approval
|
||||
- 是否需要 immigration attorney
|
||||
- evidence-building 是否 legitimate
|
||||
- 信息安全
|
||||
- O-1A 基础
|
||||
- 更多证据是否总是更好
|
||||
- top-tier outlet 是否必须
|
||||
- hackathon judging 是否有帮助
|
||||
- timeline 和 rush concerns
|
||||
- pricing、refund、scope
|
||||
|
||||
## 13. Start Here 表单现状
|
||||
|
||||
当前不是实际 `<form>`,只是视觉字段。
|
||||
|
||||
字段视觉:
|
||||
|
||||
- Name required
|
||||
- Email required
|
||||
- LinkedIn
|
||||
- Company Website
|
||||
- Current Role / Company
|
||||
- Field or Industry
|
||||
- Attach Resume or CV optional
|
||||
- What brings you to Paprika? required
|
||||
- What recognition have you already built, if any?
|
||||
- Is there a timeline on your mind?
|
||||
|
||||
提交按钮:
|
||||
|
||||
- `<a class="button primary" href="?page=thanks">Submit Profile</a>`
|
||||
|
||||
边界:
|
||||
|
||||
- 无输入控件
|
||||
- 无校验
|
||||
- 无上传
|
||||
- 无提交接口
|
||||
- 无邮件通知
|
||||
- 无 CRM/数据库记录
|
||||
|
||||
## 14. Ask a Question 表单现状
|
||||
|
||||
当前不是实际 `<form>`,只是视觉字段。
|
||||
|
||||
字段视觉:
|
||||
|
||||
- Name required
|
||||
- Email required
|
||||
- LinkedIn optional
|
||||
- Attach Resume or CV optional
|
||||
- Your question required
|
||||
|
||||
提交按钮:
|
||||
|
||||
- `<a class="button primary" href="?page=thanks">Send Question</a>`
|
||||
|
||||
边界同 Start Here。
|
||||
|
||||
## 15. Legal 页面内容
|
||||
|
||||
已有页面:
|
||||
|
||||
- Terms of Service
|
||||
- Privacy Policy
|
||||
- Disclaimer
|
||||
|
||||
当前固定日期:
|
||||
|
||||
- `Last updated: July 6, 2026`
|
||||
|
||||
风险:
|
||||
|
||||
- 日期已修正为当前工作日期;正式发布前仍需确认是否应改为实际发布日期。
|
||||
- 法务文本涉及 immigration、attorney referral、not a law firm、privacy、refund、payment、third-party services 等高风险内容,必须由负责人或法律顾问确认。
|
||||
|
||||
## 16. 当前已知缺陷和风险
|
||||
|
||||
| 编号 | 问题 | 影响 | 建议 |
|
||||
| --- | --- | --- | --- |
|
||||
| RISK-001 | Services 中 04/05/06 CTA 指向不存在的 `contact` 页面 | 点击后回退首页,转化断点 | 改为 `start` 或新增 `contact` |
|
||||
| RISK-002 | Start/Ask 不是表单 | 无法收集线索 | 改成真实 `<form>` 并接后端或第三方表单服务 |
|
||||
| RISK-003 | 法务页面日期晚于当前日期 | 发布可信度和合规风险 | 发布前确认日期 |
|
||||
| RISK-004 | outlet/network 名称可能暗示合作关系 | 法律和商业陈述风险 | 确认授权或改为“representative examples/types” |
|
||||
| RISK-005 | 无 SEO/OG/sitemap/robots | 正式站收录和分享效果弱 | 上线前补齐 |
|
||||
| RISK-006 | 无截图回归和移动端验收 | 可能存在响应式问题 | 引入 Playwright 或人工截图台账 |
|
||||
| RISK-007 | 无部署配置 | 不能可重复发布 | 建立服务、反向代理、HTTPS 和部署脚本 |
|
||||
| RISK-008 | PostgreSQL schema 尚未设计 | 表单无法稳定入库和追踪 | 建立 `submissions`、`submission_files` 等数据表 |
|
||||
| RISK-009 | 文件上传存储未定 | CV/resume 涉及隐私和访问控制 | 先确定本地磁盘、对象存储或禁用上传 |
|
||||
| RISK-010 | 邮箱统一初始密码 | 多账号共用弱初始密码有安全风险 | 首次登录强制修改或生成独立强密码 |
|
||||
| RISK-011 | 网站联系邮箱域名不一致 | `support@meetpaprika.com` 与 `paprikalaw.com` 品牌不统一 | 上线前统一 contact/support/legal 邮箱 |
|
||||
|
||||
## 17. 视觉设计对标
|
||||
|
||||
`doc/index.html` 的视觉风格不是通用模板,而是一套完整的 Paprika 品牌 mockup。后续重构或拆分必须保持以下基线,除非在 `06-决策记录.md` 新增变更决策。
|
||||
|
||||
### 17.1 CSS 变量
|
||||
|
||||
| 变量 | 值 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `--red` | `#d9432f` | 主品牌红、按钮、kicker、编号 |
|
||||
| `--deep-red` | `#a9372d` | 深红强调、active、结论句 |
|
||||
| `--ink` | `#241b14` | 主文字色 |
|
||||
| `--text` | `#5c5249` | 正文灰褐色 |
|
||||
| `--muted` | `#8d847a` | 次级文字 |
|
||||
| `--line` | `#e9e1d8` | 边线 |
|
||||
| `--paper` | `#fffdfa` | 页面底色 |
|
||||
| `--panel` | `rgba(255,255,255,.86)` | 卡片背景 |
|
||||
| `--green` | `#4f7d61` | 少量绿色标签 |
|
||||
| `--header-text` | `#41505a` | 导航文字 |
|
||||
|
||||
### 17.2 字体和排版
|
||||
|
||||
- 全站主字体:`Avenir`, `"Avenir Next"`, system sans-serif。
|
||||
- 强调语句和 italic accent 使用 `Georgia`, `"Times New Roman"`, serif。
|
||||
- 页面不使用图片资源,视觉主要由排版、红色强调、浅纸色背景、网格纹理、卡片阴影组成。
|
||||
- `h1` 首页字号范围为 `clamp(54px, 6.3vw, 74px)`。
|
||||
- 子页 `h1` 字号范围为 `clamp(44px, 5.2vw, 66px)`。
|
||||
- 主要 section title 使用 `clamp(34px, 4vw, 46px)` 或更大的 `clamp(48px, 6vw, 74px)`。
|
||||
|
||||
### 17.3 背景和布局
|
||||
|
||||
- `body` 背景由两层浅网格线、中心 radial glow 和 `--paper` 组成。
|
||||
- Header 高度 `92px`,sticky top,半透明纸色背景,`backdrop-filter: blur(14px)`。
|
||||
- 主内容容器常用:
|
||||
- `.hero`:`width: min(1040px, calc(100% - 56px))`
|
||||
- `.cards/.section/.form-wrap`:`width: min(1160px, calc(100% - 72px))`
|
||||
- `.legal-doc`:`width: min(920px, calc(100% - 72px))`
|
||||
- 卡片圆角主要是 `4px` 或 `8px`,按钮和 chip 使用 `999px`。
|
||||
|
||||
### 17.4 响应式规则
|
||||
|
||||
唯一显式断点:`@media (max-width: 1080px)`。
|
||||
|
||||
断点内行为:
|
||||
|
||||
- Header 允许换行,`.nav` 占满宽度并 flex-wrap。
|
||||
- `.cards`、`.two-col`、`.grid`、`.split-feature`、`.avoid-grid`、`.why-v2-grid`、`.criteria-grid`、`.faq-section`、`.form-wrap`、`.questions`、`.steps` 等多列布局统一变为单列。
|
||||
- `.field.large`、`.form-note` 从跨两列改为单列。
|
||||
- `.pricing-band` 改为单列并居中。
|
||||
- `.press-check-item` 从 `46px 1fr` 调整为 `38px 1fr`。
|
||||
|
||||
注意:当前只有一个 1080px 断点,未针对窄屏 header 高度、FAQ hero nowrap、长 outlet chip、法务长段落做更细处理,移动端必须截图验收。
|
||||
|
||||
## 18. 组件和 CSS 类对标
|
||||
|
||||
| 模块 | 关键类 | 当前用途 |
|
||||
| --- | --- | --- |
|
||||
| Header | `.site-header`, `.brand`, `.brand-name`, `.brand-tag`, `.nav`, `.nav-dropdown`, `.resources-menu`, `.start` | 顶部品牌、导航、下拉和 CTA |
|
||||
| Hero | `.hero`, `.subpage`, `.hero-kicker`, `.subhead`, `.actions`, `.button`, `.microcopy`, `.time-line` | 首页和子页首屏 |
|
||||
| 服务卡 | `.cards`, `.card`, `.meta`, `.num`, `.label`, `.explore` | 首页和 Services 入口 |
|
||||
| 通用内容 | `.section`, `.section-head`, `.section-title`, `.section-copy`, `.two-col`, `.grid`, `.panel`, `.mini`, `.article` | 双栏、四栏、文章卡片 |
|
||||
| CTA | `.services-cta`, `.services-cta-inner`, `.button-arrow` | 服务转化横幅 |
|
||||
| Why | `.why-v2`, `.why-v2-grid`, `.why-card`, `.why-v2-peace`, `.why-card-line` | Why Paprika 页面 |
|
||||
| Resources | `.criteria-page`, `.criteria-intro`, `.criteria-grid`, `.criterion-card`, `.criterion-block`, `.criteria-disclaimer` | O-1A criteria 页面 |
|
||||
| FAQ | `.faq-page`, `.faq-section`, `.faq-section-head`, `.faq-list`, `.faq-item`, `.faq-num` | FAQ 页面 |
|
||||
| Press | `.press-cadence`, `.press-standard`, `.press-check-card`, `.outlet-network`, `.outlet-grid`, `.avoid-section`, `.avoid-card`, `.avoid-explain` | Press 页面 |
|
||||
| Judging | `.judging-counts`, `.judging-counts-grid`, `.judging-network`, `.judging-network-chip`, `.judging-package`, `.check-dot` | Judging 页面 |
|
||||
| Process | `.process-section`, `.steps`, `.step`, `.step-num`, `.pricing-band` | 首页四步流程和价格 |
|
||||
| Form mockup | `.form-wrap`, `.field`, `.upload`, `.upload-inline`, `.large`, `.question-field`, `.start-question-field`, `.form-note`, `.req` | Start/Ask 视觉表单 |
|
||||
| Legal | `.legal-doc`, `.legal-doc-head`, `.legal-date`, `.legal-sections`, `.legal-section` | Terms/Privacy/Disclaimer |
|
||||
| Footer | `footer`, `.footer-links` | 底部链接和固定声明 |
|
||||
|
||||
已存在但当前页面未使用或未公开使用的样式/函数相关模块:
|
||||
|
||||
- `.resource-hub`, `.resource-tabs`, `.resource-group`, `.resource-card`:对应 `resourcesHub()`,当前 `pageData.resources` 使用的是 `o1CriteriaPage()`,未使用 hub。
|
||||
- `.why-proof`, `.why-reason`, `.why-closing`:疑似旧版 Why 结构,当前 `whyPaprikaProof()` 使用 `.why-v2`。
|
||||
- `pressQuality()`:旧版 press 对比区块,当前 `pressPage()` 未调用。
|
||||
- `standardGrid()`:标准 mini grid,当前未调用。
|
||||
|
||||
## 19. JavaScript 函数对标
|
||||
|
||||
| 函数/常量 | 当前状态 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `serviceCards` | 使用中 | 首页三张服务卡 HTML 片段 |
|
||||
| `pageData` | 使用中 | 所有路由页面数据表 |
|
||||
| `hero()` | 使用中 | 通用子页 hero,含 Start/Ask CTA |
|
||||
| `heroWithKicker()` | 使用中 | 带 kicker 的 hero,可关闭 actions |
|
||||
| `servicesCta()` | 使用中 | 红色服务 CTA 横幅 |
|
||||
| `card()` | 使用中 | 服务卡片 |
|
||||
| `panel()` | 使用中 | 双栏 panel |
|
||||
| `productPage()` | 当前未调用 | 通用产品页工厂 |
|
||||
| `judgingPage()` | 使用中 | Judging 页面组合 |
|
||||
| `scholarlyPage()` | 使用中 | Scholarly 页面组合 |
|
||||
| `productPageWithKicker()` | 当前未调用 | 带 kicker 的产品页工厂 |
|
||||
| `pressPage()` | 使用中 | Press 页面组合 |
|
||||
| `pressHero()` | 使用中 | Press 专属 hero |
|
||||
| `judgingCounts()` | 使用中 | Judging counts 区块 |
|
||||
| `judgingCountCard()` | 使用中 | Judging count card |
|
||||
| `judgingNetwork()` | 使用中 | Judging network chips |
|
||||
| `judgingNetworkGroup()` | 当前未调用 | 分组 network chips |
|
||||
| `judgingPackage()` | 使用中 | Judging evidence package |
|
||||
| `judgingPackageCard()` | 使用中 | Package card |
|
||||
| `pressLookFor()` | 使用中 | Press standards checklist |
|
||||
| `outletNetwork()` | 使用中 | Representative outlet chips |
|
||||
| `pressCheck()` | 使用中 | Press checklist item |
|
||||
| `pressCadenceNote()` | 使用中 | Press cadence note |
|
||||
| `pressAvoidSection()` | 使用中 | Press avoid section |
|
||||
| `avoidCard()` | 使用中 | Avoid card |
|
||||
| `miniGrid()` | 使用中 | mini card grid |
|
||||
| `articleGrid()` | 使用中 | blog article grid |
|
||||
| `o1CriteriaPage()` | 使用中 | O-1A 8 criteria 页面 |
|
||||
| `criterionCard()` | 使用中 | Criteria card |
|
||||
| `faqPage()` | 使用中 | FAQ 页面 |
|
||||
| `faqGroup()` | 使用中 | FAQ 分组 |
|
||||
| `faqItem()` | 使用中 | FAQ 单项 |
|
||||
| `resourcesHub()` | 当前未调用 | Resource hub 草稿 |
|
||||
| `resourceGroup()` | 当前未调用 | Resource group 草稿 |
|
||||
| `legalPage()` | 当前未调用 | 旧版 legal page 工厂 |
|
||||
| `legalTermsPage()` | 使用中 | Terms 页面 |
|
||||
| `legalPrivacyPage()` | 使用中 | Privacy 页面 |
|
||||
| `legalDisclaimerPage()` | 使用中 | Disclaimer 页面 |
|
||||
| `disclaimerSection()` | 使用中 | Disclaimer section |
|
||||
| `disclaimerPart()` | 使用中 | Disclaimer p/ul 渲染 |
|
||||
| `whyEarlySection()` | 使用中 | 首页 Start Early |
|
||||
| `servicesIntro()` | 使用中 | 首页 Core services intro |
|
||||
| `standardPreview()` | 使用中 | 首页标准问题 |
|
||||
| `question()` | 使用中 | 标准问题 item |
|
||||
| `howItWorks()` | 使用中 | 首页四步流程和价格 |
|
||||
| `step()` | 使用中 | 流程 step |
|
||||
| `standardGrid()` | 当前未调用 | 标准 grid 草稿 |
|
||||
| `whyPaprikaProof()` | 使用中 | Why 页面 v2 内容 |
|
||||
| `whyV2Card()` | 使用中 | Why card |
|
||||
| `pressQuality()` | 当前未调用 | 旧版 Press quality 对比 |
|
||||
|
||||
最终渲染逻辑:
|
||||
|
||||
```js
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const page = params.get("page") || "home";
|
||||
const selected = pageData[page] ? page : "home";
|
||||
document.title = `Paprika - ${selected}`;
|
||||
document.getElementById("app").innerHTML = pageData[selected].body;
|
||||
```
|
||||
|
||||
active 逻辑:
|
||||
|
||||
- `[data-page]` 与 `selected` 相同时添加 `.active`。
|
||||
- `selected` 为 `faq` 或 `resources` 时,额外给 `.nav-trigger` 添加 `.active`。
|
||||
|
||||
## 20. CTA 和链接全量映射
|
||||
|
||||
| 位置 | 文案 | 目标 |
|
||||
| --- | --- | --- |
|
||||
| Header brand | Paprika | `?page=home` |
|
||||
| Header nav | Press | `?page=press` |
|
||||
| Header nav | Judging | `?page=judging` |
|
||||
| Header nav | Scholarly Articles | `?page=scholarly` |
|
||||
| Header nav | Services | `?page=services` |
|
||||
| Header nav | Why Paprika | `?page=why` |
|
||||
| Header Resources | Resources | `?page=resources` |
|
||||
| Resources menu | Frequently Asked Questions | `?page=faq` |
|
||||
| Resources menu | O-1A Criteria Overview | `?page=resources` |
|
||||
| Header CTA | Start Here | `?page=start` |
|
||||
| Footer | About | `?page=why` |
|
||||
| Footer | FAQ | `?page=faq` |
|
||||
| Footer | Terms | `?page=terms` |
|
||||
| Footer | Privacy | `?page=privacy` |
|
||||
| Footer | Disclaimer | `?page=disclaimer` |
|
||||
| Home hero | Send Us Your Profile | `?page=start` |
|
||||
| Home hero | Ask a Question | `?page=ask` |
|
||||
| 通用 hero | Start Here | `?page=start` |
|
||||
| 通用 hero | Ask a Question | `?page=ask` |
|
||||
| Services CTA | Send Us Your Profile | `?page=start` |
|
||||
| Home pricing band | See Services | `?page=services` |
|
||||
| Start mockup | Submit Profile | `?page=thanks` |
|
||||
| Ask mockup | Send Question | `?page=thanks` |
|
||||
| Thanks | Back to Homepage | `?page=home` |
|
||||
| Service card 01 | Explore Press | `?page=press` |
|
||||
| Service card 02 | Explore Judging | `?page=judging` |
|
||||
| Service card 03 | Explore Articles | `?page=scholarly` |
|
||||
| Service card 04 | Start Here | 原为 `?page=contact`,已修复为 `?page=start` |
|
||||
| Service card 05 | Start Here | 原为 `?page=contact`,已修复为 `?page=start` |
|
||||
| Service card 06 | Start Here | 原为 `?page=contact`,已修复为 `?page=start` |
|
||||
|
||||
## 21. FAQ 完整编号对标
|
||||
|
||||
当前 FAQ 共 4 组、20 个问题。
|
||||
|
||||
| 编号范围 | 分组 kicker | 分组标题 | 问题数量 |
|
||||
| --- | --- | --- | --- |
|
||||
| 01-09 | About Paprika | How Paprika works. | 9 |
|
||||
| 10-14 | O1 Visa | O-1 Basics | 5 |
|
||||
| 15-18 | Timeline | How long things take. | 4 |
|
||||
| 19-20 | Pricing Structure | Pricing, scope, and refunds | 2 |
|
||||
|
||||
需要注意的原文问题:
|
||||
|
||||
- `O1 Visa` kicker 少了 hyphen,是否改为 `O-1 Visa` 需产品确认。
|
||||
- FAQ 15 中原文 `Coverage spaced out over time demonstrate...` 主谓可能不一致。
|
||||
- FAQ 20 写了 “If we are unable to secure the agreed-upon media placements...” 可能只覆盖 media placements,不覆盖 judging/scholarly,应由法务或产品确认。
|
||||
|
||||
## 22. Legal 页面逐项结构对标
|
||||
|
||||
### Terms of Service
|
||||
|
||||
当前 Terms 共 18 个 section,但编号为 1-16、18、19,缺少 17。
|
||||
|
||||
| 编号 | 标题 |
|
||||
| --- | --- |
|
||||
| 1 | Agreement to These Terms |
|
||||
| 2 | What Paprika Provides |
|
||||
| 3 | Not a Law Firm — No Legal Advice |
|
||||
| 4 | No Attorney-Client Relationship |
|
||||
| 5 | No Guarantee of Immigration Outcomes |
|
||||
| 6 | Attorney Referrals |
|
||||
| 7 | Fees, Payment, and Refunds |
|
||||
| 8 | Client Responsibilities and Accuracy of Information |
|
||||
| 9 | Evidence Integrity |
|
||||
| 10 | Third-Party Decisions |
|
||||
| 11 | Your Materials |
|
||||
| 12 | Work Product |
|
||||
| 13 | Prohibited Uses |
|
||||
| 14 | Disclaimer of Warranties |
|
||||
| 15 | Limitation of Liability |
|
||||
| 16 | Changes to These Terms |
|
||||
| 18 | Governing Law |
|
||||
| 19 | Contact |
|
||||
|
||||
### Privacy Policy
|
||||
|
||||
当前 Privacy 共 12 个 section:
|
||||
|
||||
1. Introduction
|
||||
2. Information We Collect
|
||||
3. How We Use Your Information
|
||||
4. How We Share Your Information
|
||||
5. Data Storage and Security
|
||||
6. Data Retention
|
||||
7. Your Rights
|
||||
8. Cookies
|
||||
9. Children's Privacy
|
||||
10. International Users
|
||||
11. Changes to This Policy
|
||||
12. Contact
|
||||
|
||||
### Disclaimer
|
||||
|
||||
当前 Disclaimer 共 9 个 section:
|
||||
|
||||
1. Not a Law Firm
|
||||
2. What "Attorney-Curated" Means
|
||||
3. No Attorney-Client Relationship
|
||||
4. Independent Counsel
|
||||
5. Attorney Referral Disclosure
|
||||
6. No Outcome Guarantee
|
||||
7. User Responsibility
|
||||
8. General Information Only
|
||||
9. Questions
|
||||
|
||||
需要注意的原文问题:
|
||||
|
||||
- Disclaimer 中 `Paprika's evidence-building services is shaped...` 主谓不一致。
|
||||
- Disclaimer 中 `Use of Paprika's does not create...` 语句不完整。
|
||||
- 三个 legal 页面日期已由 `Last updated: July 10, 2026` 修正为 `Last updated: July 6, 2026`。
|
||||
- 当前日期为 `2026-07-06`,发布前仍需法务确认文本内容和最终发布日期。
|
||||
|
||||
## 23. 源码级对标结论
|
||||
|
||||
- `doc/index.html` 共 3423 行。
|
||||
- 前 2261 行主要是 CSS 和 HTML shell。
|
||||
- 2262 行后进入 body、header、main、footer、script。
|
||||
- 2300 行后定义 `serviceCards` 和 `pageData`。
|
||||
- 2428 行后定义组件/页面函数。
|
||||
- 3410 行后执行路由选择、渲染和 active 状态设置。
|
||||
- 当前没有外部依赖、图片、字体文件、构建脚本、后端接口或测试。
|
||||
|
||||
## 24. 后续推进入口
|
||||
|
||||
- 任务领取:`04-任务矩阵.md`
|
||||
- 每轮记录:`03-推进台账.md`
|
||||
- 验收证据:`05-验收证据.md`
|
||||
- 决策依据:`06-决策记录.md`
|
||||
- 开发步骤:`02-项目程序开发详细步骤.md`
|
||||
670
working/02-项目程序开发详细步骤.md
Normal file
670
working/02-项目程序开发详细步骤.md
Normal file
@@ -0,0 +1,670 @@
|
||||
# 02-项目程序开发详细步骤
|
||||
|
||||
## 1. 开发原则
|
||||
|
||||
1. 先完整对标 `doc/index.html`,再做工程化拆分。
|
||||
2. 每一轮开发只领取 `04-任务矩阵.md` 中明确编号的任务,避免重复做。
|
||||
3. 每一轮必须同步更新:
|
||||
- `03-推进台账.md`
|
||||
- `04-任务矩阵.md`
|
||||
- `05-验收证据.md`
|
||||
- 必要时更新 `06-决策记录.md`
|
||||
4. 涉及移民、法律、隐私、退款、attorney referral、表单收集的内容,必须留有验收证据和确认记录。
|
||||
5. 不把明文服务器密码写入长期文档;部署凭据用 SSH key、环境变量或受控密钥库。
|
||||
|
||||
## 2. 当前基线复现
|
||||
|
||||
目标:确认 `doc/index.html` 当前 mockup 可回溯、可打开、可作为工程化基准。
|
||||
|
||||
步骤:
|
||||
|
||||
1. 进入项目目录:
|
||||
|
||||
```bash
|
||||
cd /home/mes123456/MeetPaprika
|
||||
```
|
||||
|
||||
2. 确认文件存在:
|
||||
|
||||
```bash
|
||||
test -f doc/index.html && wc -l doc/index.html
|
||||
```
|
||||
|
||||
3. 检查当前页面路由和脚本入口:
|
||||
|
||||
```bash
|
||||
rg -n "pageData|URLSearchParams|data-page|page=start|page=ask|page=terms|page=privacy|page=disclaimer" doc/index.html
|
||||
```
|
||||
|
||||
4. 浏览器打开以下页面并记录截图:
|
||||
|
||||
- `doc/index.html?page=home`
|
||||
- `doc/index.html?page=press`
|
||||
- `doc/index.html?page=judging`
|
||||
- `doc/index.html?page=scholarly`
|
||||
- `doc/index.html?page=services`
|
||||
- `doc/index.html?page=why`
|
||||
- `doc/index.html?page=resources`
|
||||
- `doc/index.html?page=faq`
|
||||
- `doc/index.html?page=start`
|
||||
- `doc/index.html?page=ask`
|
||||
- `doc/index.html?page=terms`
|
||||
- `doc/index.html?page=privacy`
|
||||
- `doc/index.html?page=disclaimer`
|
||||
|
||||
验收标准:
|
||||
|
||||
- 所有公开路由能渲染。
|
||||
- 未识别 `?page=unknown` 回退 `home`。
|
||||
- 顶部导航 active 状态正确。
|
||||
- Resources 下拉可见且链接正确。
|
||||
- 证据记录写入 `05-验收证据.md`。
|
||||
|
||||
## 3. 阶段 A:静态 mockup 修复
|
||||
|
||||
目标:不引入构建系统,先修复当前单文件中影响体验和转化的明确问题。
|
||||
|
||||
任务:
|
||||
|
||||
1. 修复 Services 页面 04/05/06 卡片 CTA。
|
||||
- 已由 `contact` 改为 `start`。
|
||||
2. 明确 `blog` 和 `about` 是否保留。
|
||||
- 若保留,加入导航或 footer。
|
||||
- 若不保留,记录为隐藏路由。
|
||||
3. 检查 legal 页面日期。
|
||||
- 已由 `July 10, 2026` 改为 `July 6, 2026`。
|
||||
- 当前工作日期为 `2026-07-06`。
|
||||
4. 检查首页、Press、Judging、Services、FAQ、Legal 移动端布局。
|
||||
5. 补充基础 SEO:
|
||||
- description
|
||||
- canonical
|
||||
- Open Graph
|
||||
- favicon 占位或正式资源
|
||||
6. 将 mockup 表单明确标注为占位,或进入阶段 B 改为真实表单。
|
||||
|
||||
验收标准:
|
||||
|
||||
- 无明显死链。
|
||||
- 所有 CTA 指向存在页面。
|
||||
- 法务日期有明确确认。
|
||||
- 桌面和移动端核心页面无明显重叠。
|
||||
- 变更写入台账和证据。
|
||||
|
||||
## 4. 阶段 B:真实表单与线索收集
|
||||
|
||||
目标:Start Here 和 Ask a Question 从视觉 mockup 变成真实可提交入口。
|
||||
|
||||
### 推荐字段
|
||||
|
||||
Start Here:
|
||||
|
||||
- name required
|
||||
- email required
|
||||
- linkedin
|
||||
- company_website
|
||||
- current_role_company
|
||||
- field_or_industry
|
||||
- resume_cv optional upload
|
||||
- reason required
|
||||
- existing_recognition
|
||||
- timeline
|
||||
- consent/disclaimer acknowledgement
|
||||
|
||||
Ask a Question:
|
||||
|
||||
- name required
|
||||
- email required
|
||||
- linkedin optional
|
||||
- resume_cv optional upload
|
||||
- question required
|
||||
- consent/disclaimer acknowledgement
|
||||
|
||||
### 实现路径选项
|
||||
|
||||
| 方案 | 适用 | 优点 | 风险 |
|
||||
| --- | --- | --- | --- |
|
||||
| 第三方表单服务 | 快速上线 | 开发少,有后台记录 | 文件上传和隐私策略受限 |
|
||||
| 自建轻量 API | 需要控制数据 | 可控、可扩展 | 需要安全、日志、备份 |
|
||||
| CRM 直连 | 销售流程明确 | 直接进入业务系统 | 依赖第三方 API 和权限 |
|
||||
|
||||
建议优先级:
|
||||
|
||||
1. 若目标是快速 HTTPS 上线:先接第三方表单或 webhook。
|
||||
2. 若会收 CV/敏感信息:优先自建 API,加上传限制和访问控制。
|
||||
3. 若已有 CRM:按 CRM 字段建映射并保留 submission id。
|
||||
|
||||
### 表单验收标准
|
||||
|
||||
- 必填字段缺失时不能提交。
|
||||
- email 格式错误时提示。
|
||||
- 文件上传明确限制类型和大小。
|
||||
- 成功后有 submission id 或后台记录 id。
|
||||
- 失败时显示错误状态,不静默跳转。
|
||||
- disclaimer 可见:提交不创建 attorney-client relationship。
|
||||
- 验收证据包含页面截图、提交记录、job_id/report_id/submission id。
|
||||
|
||||
## 5. 阶段 C:Next.js + React + PostgreSQL 工程化
|
||||
|
||||
目标:把单文件 mockup 迁移为 Next.js + React 应用,并接入 PostgreSQL,形成可维护、可部署、可验收的正式工程。
|
||||
|
||||
### 5.1 技术架构
|
||||
|
||||
- 前端和服务端框架:Next.js + React
|
||||
- 语言:TypeScript
|
||||
- 样式:先使用全局 CSS / CSS Modules 迁移 `doc/index.html` 视觉;不强行引入 UI 框架
|
||||
- 数据库:PostgreSQL
|
||||
- ORM:Prisma
|
||||
- 表单校验:Zod 或等价 schema 校验
|
||||
- 部署:Node.js 进程监听 `8082`,Nginx/Caddy 提供 HTTPS 反向代理
|
||||
- 验收:Playwright 页面截图 + API/数据库写入验证
|
||||
|
||||
### 5.2 初始化工程
|
||||
|
||||
当前已完成初始化。保留建议命令供复建参考:
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest paprika-site --ts --eslint --app --src-dir=false
|
||||
cd paprika-site
|
||||
npm install prisma @prisma/client zod
|
||||
npx prisma init
|
||||
```
|
||||
|
||||
本项目已直接在当前根目录工程化,并保留 `doc/index.html` 为对标基线。
|
||||
|
||||
### 5.3 页面迁移
|
||||
|
||||
从 `doc/index.html` 迁移为真实路由:
|
||||
|
||||
| 旧路由 | Next.js 路由 |
|
||||
| --- | --- |
|
||||
| `?page=home` | `/` |
|
||||
| `?page=press` | `/press` |
|
||||
| `?page=judging` | `/judging` |
|
||||
| `?page=scholarly` | `/scholarly` |
|
||||
| `?page=services` | `/services` |
|
||||
| `?page=why` | `/why` |
|
||||
| `?page=resources` | `/resources` |
|
||||
| `?page=faq` | `/faq` |
|
||||
| `?page=start` | `/start` |
|
||||
| `?page=ask` | `/ask` |
|
||||
| `?page=thanks` | `/thanks` |
|
||||
| `?page=terms` | `/terms` |
|
||||
| `?page=privacy` | `/privacy` |
|
||||
| `?page=disclaimer` | `/disclaimer` |
|
||||
|
||||
隐藏路由 `blog`、`about` 先不要公开,除非产品确认。
|
||||
|
||||
### 5.4 PostgreSQL 数据模型初稿
|
||||
|
||||
建议最小表:
|
||||
|
||||
```prisma
|
||||
model Submission {
|
||||
id String @id @default(cuid())
|
||||
type String
|
||||
name String
|
||||
email String
|
||||
linkedin String?
|
||||
companyWebsite String?
|
||||
currentRoleCompany String?
|
||||
fieldOrIndustry String?
|
||||
reason String?
|
||||
existingRecognition String?
|
||||
timeline String?
|
||||
question String?
|
||||
consentAccepted Boolean @default(false)
|
||||
sourcePath String?
|
||||
status String @default("new")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
files SubmissionFile[]
|
||||
}
|
||||
|
||||
model SubmissionFile {
|
||||
id String @id @default(cuid())
|
||||
submissionId String
|
||||
fileName String
|
||||
mimeType String
|
||||
byteSize Int
|
||||
storageKey String
|
||||
createdAt DateTime @default(now())
|
||||
submission Submission @relation(fields: [submissionId], references: [id])
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `type`:`start` 或 `ask`
|
||||
- `sourcePath`:提交来源,如 `/start`、`/ask`
|
||||
- `status`:`new`、`reviewed`、`spam`、`archived`
|
||||
- 文件上传未落地前,可以先不创建 `SubmissionFile` 或禁用上传。
|
||||
|
||||
### 5.5 表单 API
|
||||
|
||||
建议 API:
|
||||
|
||||
```text
|
||||
POST /api/submissions
|
||||
```
|
||||
|
||||
请求类型:
|
||||
|
||||
- `type=start`
|
||||
- `type=ask`
|
||||
|
||||
验收:
|
||||
|
||||
- 必填字段缺失返回 400。
|
||||
- 邮箱格式错误返回 400。
|
||||
- 成功写入 PostgreSQL 并返回 `submission_id`。
|
||||
- 前端成功后跳转 `/thanks?submission_id=...` 或展示成功状态。
|
||||
- 失败时显示明确错误。
|
||||
|
||||
### 5.6 环境变量
|
||||
|
||||
`.env` 至少包含:
|
||||
|
||||
```bash
|
||||
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/paprika"
|
||||
NEXT_PUBLIC_SITE_URL="https://paprikalaw.com"
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `.env` 不提交。
|
||||
- 服务器凭据和数据库密码不写入 `working` 文档。
|
||||
- 生产数据库用户只授予应用所需权限。
|
||||
|
||||
### 5.7 服务器 PostgreSQL 验证
|
||||
|
||||
用户已确认 PostgreSQL 在服务器 `170.106.192.152` 安装完成。正式接入前需要在服务器上验证:
|
||||
|
||||
```bash
|
||||
ssh ubuntu@170.106.192.152
|
||||
systemctl status postgresql --no-pager
|
||||
psql --version
|
||||
sudo -u postgres psql -c '\l'
|
||||
```
|
||||
|
||||
建议创建应用数据库和受限应用用户:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE paprika;
|
||||
CREATE USER paprika_app WITH ENCRYPTED PASSWORD 'REPLACE_WITH_SECURE_PASSWORD';
|
||||
GRANT CONNECT ON DATABASE paprika TO paprika_app;
|
||||
```
|
||||
|
||||
进入数据库后授予 schema 权限:
|
||||
|
||||
```sql
|
||||
\c paprika
|
||||
GRANT USAGE, CREATE ON SCHEMA public TO paprika_app;
|
||||
```
|
||||
|
||||
生产 `DATABASE_URL` 示例:
|
||||
|
||||
```bash
|
||||
DATABASE_URL="postgresql://paprika_app:REPLACE_WITH_SECURE_PASSWORD@127.0.0.1:5432/paprika"
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 不使用 SSH 登录密码作为数据库密码。
|
||||
- 不把数据库密码写入仓库、`working` 文档或聊天后的可复制文档。
|
||||
- 如果 PostgreSQL 仅供本机 Next.js 使用,优先监听 `127.0.0.1`,不要对公网开放 5432。
|
||||
|
||||
### 5.8 构建和本地验证
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run generate:legacy
|
||||
npm run prisma:generate
|
||||
npm run lint
|
||||
npx tsc --noEmit
|
||||
npm run build
|
||||
npm run start -- -p 8082
|
||||
```
|
||||
|
||||
本地生产服务冒烟:
|
||||
|
||||
```bash
|
||||
for p in / /press /judging /scholarly /services /why /resources /faq /start /ask /thanks /terms /privacy /disclaimer /robots.txt /sitemap.xml; do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8082$p)
|
||||
printf '%s %s\n' "$code" "$p"
|
||||
done
|
||||
```
|
||||
|
||||
生产数据库迁移:
|
||||
|
||||
```bash
|
||||
npx prisma migrate deploy
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- `npm run dev` 可本地启动。
|
||||
- `npm run build` 通过。
|
||||
- `npm run start -- -p 8082` 可在 8082 启动。
|
||||
- `npx prisma migrate deploy` 可在生产环境执行。
|
||||
- 核心页面内容、视觉和 CTA 与 `doc/index.html` 对标。
|
||||
- Start/Ask 提交能写入 PostgreSQL。当前本地未配置 `DATABASE_URL` 时只能验证 400 校验路径和 500 失败路径,不能关闭入库验收。
|
||||
|
||||
## 6. 阶段 D:部署到云服务器
|
||||
|
||||
目标:让 `https://paprikalaw.com/` 访问目标站点,内部服务端口使用 `8082`。
|
||||
|
||||
### 服务器信息
|
||||
|
||||
- Host:`170.106.192.152`
|
||||
- User:`ubuntu`
|
||||
- 端口范围:`8080-8099` 已开放
|
||||
- 本项目端口:`8082`
|
||||
- 密码:不写入文档;使用用户交接的临时凭据登录后立即配置 SSH key
|
||||
|
||||
### 建议部署结构
|
||||
|
||||
```text
|
||||
/opt/paprika/
|
||||
current/
|
||||
releases/
|
||||
shared/
|
||||
.env
|
||||
logs/
|
||||
```
|
||||
|
||||
### 基础命令模板
|
||||
|
||||
```bash
|
||||
ssh ubuntu@170.106.192.152
|
||||
sudo mkdir -p /opt/paprika/releases /opt/paprika/shared/logs
|
||||
sudo chown -R ubuntu:ubuntu /opt/paprika
|
||||
```
|
||||
|
||||
### Next.js 服务示例
|
||||
|
||||
构建并启动:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run build
|
||||
npm run start -- -p 8082
|
||||
```
|
||||
|
||||
生产建议用 systemd:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Paprika static site
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
WorkingDirectory=/opt/paprika/current
|
||||
Environment=NODE_ENV=production
|
||||
EnvironmentFile=/opt/paprika/shared/.env
|
||||
ExecStart=/usr/bin/npm run start -- -p 8082
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Nginx 反向代理示例
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name paprikalaw.com www.paprikalaw.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8082;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HTTPS:
|
||||
|
||||
```bash
|
||||
sudo certbot --nginx -d paprikalaw.com -d www.paprikalaw.com
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- `curl -I http://127.0.0.1:8082` 在服务器上返回 200。
|
||||
- `curl -I https://paprikalaw.com/` 返回 200。
|
||||
- `https://paprikalaw.com/`、`/start`、`/faq` 可访问。
|
||||
- `POST /api/submissions` 能写入 PostgreSQL 并返回 `submission_id`。
|
||||
- systemd 服务 `active (running)`。
|
||||
- Nginx 配置通过 `sudo nginx -t`。
|
||||
- HTTPS 证书有效。
|
||||
|
||||
## 7. 阶段 E:回归验收
|
||||
|
||||
目标:上线前后能证明页面、表单、合规、部署都可用。
|
||||
|
||||
### 页面回归
|
||||
|
||||
必测页面:
|
||||
|
||||
- home
|
||||
- press
|
||||
- judging
|
||||
- scholarly
|
||||
- services
|
||||
- why
|
||||
- resources
|
||||
- faq
|
||||
- start
|
||||
- ask
|
||||
- thanks
|
||||
- terms
|
||||
- privacy
|
||||
- disclaimer
|
||||
|
||||
### 设备尺寸
|
||||
|
||||
- Desktop:1440x900
|
||||
- Tablet:768x1024
|
||||
- Mobile:390x844
|
||||
|
||||
### 验收内容
|
||||
|
||||
- 页面可访问。
|
||||
- 首屏品牌和 CTA 可见。
|
||||
- Header 不遮挡内容。
|
||||
- 文本无明显重叠。
|
||||
- CTA 链接可达。
|
||||
- 表单可提交或明确是 mockup。
|
||||
- Footer legal 链接可达。
|
||||
- Legal disclaimer 可见。
|
||||
|
||||
### 证据格式
|
||||
|
||||
每次验收写入 `05-验收证据.md`:
|
||||
|
||||
```text
|
||||
EV-YYYYMMDD-序号
|
||||
时间:
|
||||
任务:
|
||||
命令/页面:
|
||||
结果:
|
||||
证据文件:
|
||||
job_id/report_id/submission_id:
|
||||
结论:
|
||||
```
|
||||
|
||||
## 8. 阶段 F:后续产品增强
|
||||
|
||||
可选任务:
|
||||
|
||||
- 增加真实 Contact 页面。
|
||||
- 增加 Blog/Resources 内容系统。
|
||||
- 增加案例或证据包示例,但必须避免误导和隐私泄露。
|
||||
- 增加 analytics,并同步 Privacy Policy。
|
||||
- 增加 sitemap、robots、structured data。
|
||||
- 增加 Playwright screenshot regression。
|
||||
- 增加后台 leads 管理或 CRM 同步。
|
||||
|
||||
## 9. 阶段 G:邮箱系统配置与验证
|
||||
|
||||
目标:确认 `mail.paprikalaw.com` 邮件系统可用,管理员和普通公司邮箱能登录、收发和承接网站通知。
|
||||
|
||||
### 9.1 邮箱入口
|
||||
|
||||
- 普通用户网页登录:`https://mail.paprikalaw.com/webmail/`
|
||||
- 管理员网页登录:`https://mail.paprikalaw.com/admin/`
|
||||
- 管理员邮箱:`admin@paprikalaw.com`
|
||||
|
||||
### 9.2 需要创建或确认的普通邮箱
|
||||
|
||||
```text
|
||||
info@paprikalaw.com
|
||||
contact@paprikalaw.com
|
||||
support@paprikalaw.com
|
||||
sales@paprikalaw.com
|
||||
marketing@paprikalaw.com
|
||||
hr@paprikalaw.com
|
||||
finance@paprikalaw.com
|
||||
billing@paprikalaw.com
|
||||
legal@paprikalaw.com
|
||||
service@paprikalaw.com
|
||||
```
|
||||
|
||||
凭据规则:
|
||||
|
||||
- 用户已提供统一初始密码,但不写入长期文档。
|
||||
- 建议创建后立即为每个邮箱设置独立强密码。
|
||||
- 如果邮件系统支持,启用首次登录改密、2FA、登录审计和反垃圾策略。
|
||||
|
||||
### 9.3 DNS 和投递验证
|
||||
|
||||
需要确认:
|
||||
|
||||
- `mail.paprikalaw.com` A/AAAA 记录正确。
|
||||
- `paprikalaw.com` MX 记录指向邮件服务器。
|
||||
- SPF 记录包含授权发件源。
|
||||
- DKIM 已生成并加入 DNS。
|
||||
- DMARC 已配置,至少先从 `p=none` 观察。
|
||||
- 反向 DNS/PTR 与邮件服务器发信域名匹配。
|
||||
|
||||
建议验证:
|
||||
|
||||
```bash
|
||||
dig MX paprikalaw.com
|
||||
dig TXT paprikalaw.com
|
||||
dig TXT default._domainkey.paprikalaw.com
|
||||
dig TXT _dmarc.paprikalaw.com
|
||||
```
|
||||
|
||||
### 9.4 网站表单通知接入
|
||||
|
||||
Next.js 表单写入 PostgreSQL 后,可选邮件通知:
|
||||
|
||||
- 发件邮箱:建议 `support@paprikalaw.com` 或专用 `no-reply@paprikalaw.com`
|
||||
- 收件邮箱:建议 `support@paprikalaw.com`、`contact@paprikalaw.com`
|
||||
- 表单类型:
|
||||
- `/start` 提交通知
|
||||
- `/ask` 提交通知
|
||||
- 邮件内容不得包含完整敏感 CV 文件,只发送 submission id 和后台查看提示。
|
||||
|
||||
环境变量示例:
|
||||
|
||||
```bash
|
||||
SMTP_HOST="mail.paprikalaw.com"
|
||||
SMTP_PORT="587"
|
||||
SMTP_USER="support@paprikalaw.com"
|
||||
SMTP_PASSWORD="REPLACE_WITH_SECURE_PASSWORD"
|
||||
LEADS_NOTIFY_TO="support@paprikalaw.com,contact@paprikalaw.com"
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
- 管理员能登录 admin。
|
||||
- 10 个普通邮箱能登录 webmail。
|
||||
- 至少完成一封内部互发、一封外部收信、一封外部发信测试。
|
||||
- SPF/DKIM/DMARC 检查通过或有明确待处理项。
|
||||
- 表单邮件通知如启用,必须记录 message id 或测试截图。
|
||||
|
||||
## 10. 每轮工作固定流程
|
||||
|
||||
1. 查看 `04-任务矩阵.md`,领取一个或一组任务。
|
||||
2. 查看相关决策:`06-决策记录.md`。
|
||||
3. 修改代码或文档。
|
||||
4. 执行对应验证。
|
||||
5. 将证据写入 `05-验收证据.md`。
|
||||
6. 更新任务状态。
|
||||
7. 在 `03-推进台账.md` 记录:
|
||||
- 本轮做了什么
|
||||
- 改了哪些文件
|
||||
- 验证了什么
|
||||
- 下一步是什么
|
||||
|
||||
## 11. 完全对标核验清单
|
||||
|
||||
每次声称“已完全对标 `doc/index.html`”时,至少执行以下核验。
|
||||
|
||||
### 10.1 源文件结构核验
|
||||
|
||||
```bash
|
||||
wc -l doc/index.html
|
||||
rg -n 'function [A-Za-z0-9_]+\(|const [A-Za-z0-9_]+ =|href="\?page=|Last updated|Packages start|@media|--[a-z-]+:' doc/index.html
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- 记录 `doc/index.html` 行数。
|
||||
- 记录 CSS token、断点、路由、CTA、函数、法务日期、价格文案。
|
||||
|
||||
### 10.2 页面和函数映射核验
|
||||
|
||||
```bash
|
||||
rg -n 'pageData|home:|press:|judging:|scholarly:|services:|why:|resources:|faq:|blog:|start:|ask:|thanks:|about:|terms:|privacy:|disclaimer:' doc/index.html
|
||||
rg -n 'function (hero|heroWithKicker|servicesCta|card|pressPage|judgingPage|scholarlyPage|o1CriteriaPage|faqPage|legalTermsPage|legalPrivacyPage|legalDisclaimerPage)' doc/index.html
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- `01-项目功能内容.md` 中的路由清单与 `pageData` 保持一致。
|
||||
- 使用中函数、未调用函数都有记录。
|
||||
- 隐藏路由 `blog`、`about` 不被误删。
|
||||
|
||||
### 10.3 链接和 CTA 核验
|
||||
|
||||
```bash
|
||||
rg -n 'href="\?page=|card\("' doc/index.html
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- Header、footer、hero、services CTA、form mockup、thanks、service card 链接全部登记。
|
||||
- 不存在的 `contact` 路由历史缺陷必须保留在风险和任务矩阵中;修复后记录验收证据。
|
||||
|
||||
### 10.4 Legal 和 FAQ 核验
|
||||
|
||||
```bash
|
||||
sed -n '2870,3035p' doc/index.html
|
||||
sed -n '3070,3228p' doc/index.html
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- FAQ 4 组 20 个问题记录完整。
|
||||
- Terms、Privacy、Disclaimer section 数量和标题记录完整。
|
||||
- Terms 缺少 17、legal 日期晚于当前日期、原文语法风险均进入任务矩阵。
|
||||
|
||||
### 10.5 视觉样式核验
|
||||
|
||||
```bash
|
||||
rg -n '^\\s*\\.[A-Za-z0-9_-]+|@media|grid-template-columns|width: min|border-radius|background:' doc/index.html
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- CSS token、布局宽度、卡片、表单、Press、Judging、FAQ、Legal、响应式断点在文档中有对应说明。
|
||||
- 后续工程化拆分不能只迁移内容,必须迁移视觉规则。
|
||||
253
working/03-推进台账.md
Normal file
253
working/03-推进台账.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# 03-推进台账
|
||||
|
||||
本文件按轮次记录工作。每轮结束必须写清楚:做了什么、改了哪些文件、验证了什么、下一步是什么。
|
||||
|
||||
## R001 - 建立 working 文档体系
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 任务编号:T001、T002
|
||||
- 做了什么:
|
||||
- 阅读 `doc/index.html` 的页面结构、导航、路由、表单 mockup、服务模块、FAQ、O-1A criteria、法务页面和渲染脚本。
|
||||
- 建立 `working` 目录下 6 份编号文档和 README 索引。
|
||||
- 初步记录当前项目是自包含静态 HTML mockup。
|
||||
- 改了哪些文件:
|
||||
- `working/README.md`
|
||||
- `working/01-项目功能内容.md`
|
||||
- `working/02-项目程序开发详细步骤.md`
|
||||
- `working/03-推进台账.md`
|
||||
- `working/04-任务矩阵.md`
|
||||
- `working/05-验收证据.md`
|
||||
- `working/06-决策记录.md`
|
||||
- 验证了什么:
|
||||
- 确认 `doc/index.html` 是自包含静态 HTML。
|
||||
- 确认页面通过 `?page=...` 查询参数切换。
|
||||
- 确认 Start Here 和 Ask a Question 当前是视觉 mockup,不是真实表单。
|
||||
- 证据链接或编号:E001、E002、E003
|
||||
- 遗留问题:
|
||||
- 未做浏览器截图验收。
|
||||
- 未做移动端回归。
|
||||
- 未实现表单和部署。
|
||||
- 下一步:
|
||||
- 按 `04-任务矩阵.md` 从 `VERIFY` 与 `TODO` 任务继续推进。
|
||||
|
||||
## R002 - 完全对标 doc/index.html 补强 working 文档
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 任务编号:T028
|
||||
- 做了什么:
|
||||
- 复读 `doc/index.html` 中 header、footer、`pageData`、页面组合函数、Press/Judging/Resources/FAQ/Legal/Form 模块。
|
||||
- 补充 `01-项目功能内容.md`,把页面路由、内容模块、表单字段、服务卡片、O-1A criteria、法律页面、已知缺陷和发布环境写完整。
|
||||
- 补充 `02-项目程序开发详细步骤.md`,形成阶段化路线:基线复现、静态修复、真实表单、工程化拆分、8082 端口部署、HTTPS、回归验收。
|
||||
- 补强任务矩阵,新增服务器部署、凭据处理、contact 死链、outlet/network 陈述风险、README 索引维护等任务。
|
||||
- 补强验收证据,记录本轮命令、源码发现和后续证据清单。
|
||||
- 补强决策记录,明确不在长期文档保存明文密码、服务默认走 8082、Services 的 `contact` 路由是缺陷。
|
||||
- 改了哪些文件:
|
||||
- `working/README.md`
|
||||
- `working/01-项目功能内容.md`
|
||||
- `working/02-项目程序开发详细步骤.md`
|
||||
- `working/03-推进台账.md`
|
||||
- `working/04-任务矩阵.md`
|
||||
- `working/05-验收证据.md`
|
||||
- `working/06-决策记录.md`
|
||||
- 验证了什么:
|
||||
- `rg --files` 确认项目当前主要文件为 `doc/index.html`、`doc/README.md` 和 `working/*.md`。
|
||||
- `rg -n` 确认公开路由、隐藏路由、表单 mockup、legal 页面、动态路由逻辑均在 `doc/index.html`。
|
||||
- `sed` 分段读取源文件,确认 Services 04/05/06 卡片指向不存在的 `contact` 页面。
|
||||
- `git status --short` 失败,确认当前目录不是 Git 仓库或未在 Git 工作树内。
|
||||
- 证据链接或编号:E004、E005、E006、E007
|
||||
- 遗留问题:
|
||||
- 尚未改 `doc/index.html`。
|
||||
- 尚未启动本地服务器或做截图。
|
||||
- 尚未登录远端服务器。
|
||||
- 用户提供了密码,但长期文档未写入明文密码。
|
||||
- 下一步:
|
||||
- 优先处理 T029:修复 Services 页面不存在的 `contact` 路由。
|
||||
- 继续 T003-T011:对核心页面做浏览器截图验收。
|
||||
- 之后按产品选择推进 T012/T013 表单落地或 T022 工程化拆分。
|
||||
|
||||
## R003 - 源码级对标附录完善
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 任务编号:T034
|
||||
- 做了什么:
|
||||
- 重新扫描 `doc/index.html` 的 CSS token、响应式断点、路由链接、函数列表、FAQ、Legal sections 和渲染逻辑。
|
||||
- 在 `01-项目功能内容.md` 增加视觉设计、组件 CSS 类、JavaScript 函数、CTA 全量映射、FAQ 完整编号、Legal 逐项结构和源码级结论。
|
||||
- 在 `02-项目程序开发详细步骤.md` 增加“完全对标核验清单”,规定以后如何证明文档与源 HTML 对齐。
|
||||
- 更新任务矩阵、验收证据和决策记录。
|
||||
- 改了哪些文件:
|
||||
- `working/01-项目功能内容.md`
|
||||
- `working/02-项目程序开发详细步骤.md`
|
||||
- `working/03-推进台账.md`
|
||||
- `working/04-任务矩阵.md`
|
||||
- `working/05-验收证据.md`
|
||||
- `working/06-决策记录.md`
|
||||
- 验证了什么:
|
||||
- `doc/index.html` 共 3423 行。
|
||||
- CSS 只有一个显式响应式断点:`@media (max-width: 1080px)`。
|
||||
- `pageData` 包含公开路由、隐藏路由和 legal 页面。
|
||||
- FAQ 共 4 组 20 个问题。
|
||||
- Terms 编号缺少 17,Legal 日期均为 `July 10, 2026`。
|
||||
- 多个函数和 CSS 模块存在但当前未调用。
|
||||
- 证据链接或编号:E016
|
||||
- 遗留问题:
|
||||
- 仍未修改 `doc/index.html` 源码。
|
||||
- 仍未做浏览器截图和真实部署验收。
|
||||
- 下一步:
|
||||
- 按 T029 修复 `contact` 缺陷,或按 T003-T011 做页面截图验收。
|
||||
|
||||
## R004 - 明确正式开发技术架构
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 任务编号:T035
|
||||
- 做了什么:
|
||||
- 根据用户确认,将正式开发路线从候选方案收敛为 Next.js + React + PostgreSQL。
|
||||
- 将默认数据访问层定为 Prisma ORM。
|
||||
- 补充 Next.js 路由迁移表、PostgreSQL 数据模型初稿、表单 API、环境变量、8082 启动方式和部署验收标准。
|
||||
- 更新任务矩阵和决策记录。
|
||||
- 改了哪些文件:
|
||||
- `working/01-项目功能内容.md`
|
||||
- `working/02-项目程序开发详细步骤.md`
|
||||
- `working/03-推进台账.md`
|
||||
- `working/04-任务矩阵.md`
|
||||
- `working/05-验收证据.md`
|
||||
- `working/06-决策记录.md`
|
||||
- `working/README.md`
|
||||
- 验证了什么:
|
||||
- 文档已不再把 Vite/静态拆分作为推荐路线。
|
||||
- 部署命令已调整为 Next.js `npm run start -- -p 8082`。
|
||||
- 数据库任务已进入矩阵。
|
||||
- 证据链接或编号:E017
|
||||
- 遗留问题:
|
||||
- 尚未实际创建 Next.js 工程。
|
||||
- 尚未创建 PostgreSQL 数据库或 Prisma migration。
|
||||
- 文件上传存储方案仍需单独确认。
|
||||
- 下一步:
|
||||
- 执行 T036 初始化 Next.js 工程。
|
||||
- 执行 T037 设计并迁移 PostgreSQL schema。
|
||||
|
||||
## R005 - 记录服务器 PostgreSQL 安装状态
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 任务编号:T040
|
||||
- 做了什么:
|
||||
- 根据用户反馈,记录 PostgreSQL 已在服务器 `170.106.192.152` 安装完成。
|
||||
- 补充服务器 PostgreSQL 验证命令、应用数据库和受限用户创建建议。
|
||||
- 保持凭据安全规则:不把明文 SSH 密码写入长期文档。
|
||||
- 改了哪些文件:
|
||||
- `working/01-项目功能内容.md`
|
||||
- `working/02-项目程序开发详细步骤.md`
|
||||
- `working/03-推进台账.md`
|
||||
- `working/04-任务矩阵.md`
|
||||
- `working/05-验收证据.md`
|
||||
- `working/06-决策记录.md`
|
||||
- 验证了什么:
|
||||
- 本轮未登录服务器验证,仅记录用户确认。
|
||||
- 证据链接或编号:E018
|
||||
- 遗留问题:
|
||||
- 需要远程执行 `systemctl status postgresql`、`psql --version`、数据库列表检查。
|
||||
- 需要创建或确认 `paprika` 数据库和 `paprika_app` 应用用户。
|
||||
- 下一步:
|
||||
- 执行 T040 远程验证 PostgreSQL。
|
||||
- 执行 T037 接入 Prisma schema 和 migration。
|
||||
|
||||
## R006 - 记录邮箱系统账号需求
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 任务编号:T041
|
||||
- 做了什么:
|
||||
- 记录普通用户 webmail 入口、管理员入口和 `admin@paprikalaw.com` 管理员账号。
|
||||
- 记录 10 个普通公司邮箱账号清单。
|
||||
- 补充邮件 DNS、收发验证、表单通知接入和凭据安全要求。
|
||||
- 明确用户提供的统一初始密码不写入长期文档。
|
||||
- 改了哪些文件:
|
||||
- `working/01-项目功能内容.md`
|
||||
- `working/02-项目程序开发详细步骤.md`
|
||||
- `working/03-推进台账.md`
|
||||
- `working/04-任务矩阵.md`
|
||||
- `working/05-验收证据.md`
|
||||
- `working/06-决策记录.md`
|
||||
- `working/README.md`
|
||||
- 验证了什么:
|
||||
- 本轮只记录需求,未登录邮件后台,未验证 DNS/MX/SPF/DKIM/DMARC。
|
||||
- 证据链接或编号:E019
|
||||
- 遗留问题:
|
||||
- 需要登录 `https://mail.paprikalaw.com/admin/` 确认账号是否已创建。
|
||||
- 需要测试 webmail 登录、内外部收发和 DNS 记录。
|
||||
- 需要决定网站表单通知使用哪个邮箱。
|
||||
- 下一步:
|
||||
- 执行 T041 邮箱账号创建/确认。
|
||||
- 执行 T042 邮件 DNS 和收发测试。
|
||||
- 执行 T043 Next.js 表单邮件通知配置。
|
||||
|
||||
## R007 - 落地 Next.js 工程、Prisma 和真实表单
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 任务编号:T012、T013、T017、T020、T022、T029、T036、T037、T038、T039、T044
|
||||
- 做了什么:
|
||||
- 修复 `doc/index.html` 中 Services 04/05/06 CTA 指向不存在 `contact` 的缺陷,统一改为 `start`。
|
||||
- 将 legal 页面日期从未来日期 `July 10, 2026` 改为当前工作日期 `July 6, 2026`。
|
||||
- 在项目根目录初始化 Next.js + React + TypeScript App Router 工程。
|
||||
- 编写 `scripts/extract-legacy-pages.mjs`,从 `doc/index.html` 抽取 CSS 和静态页面 HTML,生成 `app/globals.css` 与 `lib/legacy-pages.ts`。
|
||||
- 建立 `app/[page]/page.tsx` 静态路由,迁移公开页面到 `/press`、`/judging`、`/services` 等路径路由。
|
||||
- 建立 `/start`、`/ask` 真实 React 表单,提交到 `POST /api/submissions`。
|
||||
- 建立 Prisma schema、初始化 migration、`.env.example`、数据库访问层和 Zod 校验。
|
||||
- 增加基础 SEO metadata、`robots.txt` 和 `sitemap.xml`。
|
||||
- 升级到 Next `16.2.10`、React `19.2.7`,并配置 ESLint 9 flat config。
|
||||
- 改了哪些文件:
|
||||
- `doc/index.html`
|
||||
- `.env.example`
|
||||
- `.gitignore`
|
||||
- `package.json`
|
||||
- `package-lock.json`
|
||||
- `tsconfig.json`
|
||||
- `next-env.d.ts`
|
||||
- `next.config.mjs`
|
||||
- `eslint.config.mjs`
|
||||
- `scripts/extract-legacy-pages.mjs`
|
||||
- `app/*`
|
||||
- `components/*`
|
||||
- `lib/*`
|
||||
- `prisma/schema.prisma`
|
||||
- `prisma/migrations/20260706120000_init/migration.sql`
|
||||
- `working/README.md`
|
||||
- `working/01-项目功能内容.md`
|
||||
- `working/02-项目程序开发详细步骤.md`
|
||||
- `working/03-推进台账.md`
|
||||
- `working/04-任务矩阵.md`
|
||||
- `working/05-验收证据.md`
|
||||
- `working/06-决策记录.md`
|
||||
- 验证了什么:
|
||||
- `npm run generate:legacy` 成功生成 12 个旧页面数据。
|
||||
- `npm run prisma:generate` 成功生成 Prisma Client。
|
||||
- `npm run lint` 通过。
|
||||
- `npx tsc --noEmit` 通过。
|
||||
- `npm run build` 通过,生成 19 个 app route。
|
||||
- `npm run start -- -p 8082` 本地生产服务可启动。
|
||||
- `/`、`/press`、`/judging`、`/scholarly`、`/services`、`/why`、`/resources`、`/faq`、`/start`、`/ask`、`/thanks`、`/terms`、`/privacy`、`/disclaimer`、`/robots.txt`、`/sitemap.xml` 本地生产服务均返回 200。
|
||||
- `/api/submissions` 对缺失必填字段和错误 email 返回 400 校验错误。
|
||||
- 有效提交在未配置 `DATABASE_URL` 时返回 500,确认入库成功仍需真实 PostgreSQL。
|
||||
- 证据链接或编号:E022、E023、E024、E025、E026
|
||||
- 遗留问题:
|
||||
- 本地未配置 PostgreSQL `DATABASE_URL`,尚不能完成真实入库验收。
|
||||
- 远程 PostgreSQL、应用数据库、应用用户、migration deploy 尚未验证。
|
||||
- 邮箱后台、DNS、SMTP 通知未验证。
|
||||
- 法务内容审核、outlet/network 名称确认、文件上传策略仍需外部确认。
|
||||
- 尚未做浏览器截图/PDF 归档。
|
||||
- 下一步:
|
||||
- 执行 T040/T044:远程验证 PostgreSQL,配置生产 `DATABASE_URL`,执行 migration,并提交 Start/Ask 获取 `submission_id`。
|
||||
- 执行 T039/T024:部署 Next.js 到服务器 `8082` 并配置 systemd。
|
||||
- 执行 T041-T043:验证邮箱和 SMTP 通知。
|
||||
- 执行 T018/T026:补桌面和移动端截图证据。
|
||||
|
||||
## 后续轮次模板
|
||||
|
||||
### RXXX - 标题
|
||||
|
||||
- 日期:
|
||||
- 任务编号:
|
||||
- 做了什么:
|
||||
- 改了哪些文件:
|
||||
- 验证了什么:
|
||||
- 证据链接或编号:
|
||||
- 遗留问题:
|
||||
- 下一步:
|
||||
81
working/04-任务矩阵.md
Normal file
81
working/04-任务矩阵.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 04-任务矩阵
|
||||
|
||||
状态定义:
|
||||
|
||||
- `DONE`:已完成并有证据。
|
||||
- `VERIFY`:源码看起来具备,但缺浏览器、截图、端到端或人工确认。
|
||||
- `TODO`:尚未实现。
|
||||
- `BLOCKED`:被外部输入、账号、法律审核、DNS 或产品决策阻塞。
|
||||
- `DEFER`:暂缓,不影响当前阶段交付。
|
||||
|
||||
## 任务总表
|
||||
|
||||
| 编号 | 任务 | 状态 | 验收标准 | 依赖 | 备注 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| T001 | 建立 working 文档体系 | DONE | 6 份编号文档和 README 索引存在 | 无 | R001 |
|
||||
| T002 | 盘点当前页面路由 | DONE | 路由清单覆盖公开路由、隐藏路由和回退逻辑 | T001 | 见 01 |
|
||||
| T003 | 顶部导航可用 | VERIFY | 点击 Press/Judging/Scholarly/Services/Why/Start 能进入对应页面,active 状态正确 | T002 | 需截图 |
|
||||
| T004 | Resources 下拉可用 | VERIFY | hover/focus 显示 FAQ 和 O-1A Criteria Overview,链接正确 | T002 | 需桌面验证 |
|
||||
| T005 | 首页内容完整 | VERIFY | hero、三大服务、Start Early、标准问题、四步流程、价格 band 渲染正常 | T002 | 需截图 |
|
||||
| T006 | Services 页面完整 | VERIFY | 6 张服务卡片和说明渲染正常 | T002 | 存在 T029 缺陷 |
|
||||
| T007 | Press 页面完整 | VERIFY | press hero、cadence、standards、avoid、outlet network、CTA 渲染正常 | T002 | 需截图 |
|
||||
| T008 | Judging 页面完整 | VERIFY | judging counts、network、evidence package、CTA 渲染正常 | T002 | 需截图 |
|
||||
| T009 | Scholarly 页面完整 | VERIFY | hero、两栏说明、CTA 渲染正常 | T002 | 需截图 |
|
||||
| T010 | Resources/O-1A criteria 页面完整 | VERIFY | 8 项 criteria 全部展示,免责声明展示 | T002 | 需内容抽查 |
|
||||
| T011 | FAQ 页面完整 | VERIFY | FAQ 分组和问题渲染正常 | T002 | 需内容抽查 |
|
||||
| T012 | Start Here 表单真实可用 | VERIFY | 真实 `<form>` 或等价组件;必填校验;提交后产生可追踪记录 | T037/T044 | React 表单和 API 已实现,待真实 PostgreSQL 入库证据 |
|
||||
| T013 | Ask a Question 表单真实可用 | VERIFY | 真实 `<form>` 或等价组件;必填校验;提交后产生可追踪记录 | T037/T044 | React 表单和 API 已实现,待真实 PostgreSQL 入库证据 |
|
||||
| T014 | 文件上传策略 | DEFER | 明确支持或不支持上传;若支持,限制类型、大小、存储、访问权限 | T012/T013 | 当前代码禁用上传,待隐私/存储方案确认 |
|
||||
| T015 | Thanks 页面流程验收 | VERIFY | 成功提交后进入 thanks;直接访问 thanks 不造成误导 | T012/T013 | 当前仅链接跳转 |
|
||||
| T016 | 法务页面内容审核 | BLOCKED | Terms、Privacy、Disclaimer 经负责人或律师确认 | 法务输入 | 当前为草稿 |
|
||||
| T017 | 法务日期修正 | DONE | 页面日期不晚于实际发布日,或明确未来生效逻辑 | T016 | 已改为 `July 6, 2026`,内容审核仍见 T016 |
|
||||
| T018 | 响应式布局验证 | TODO | 1440、768、390 宽度核心页面无重叠、溢出、不可读 | T003-T011 | 需截图 |
|
||||
| T019 | 无障碍基础检查 | TODO | heading 层级合理;交互元素可聚焦;颜色对比和表单标签达标 | T012/T013 | 当前表单不是 input |
|
||||
| T020 | SEO 基础 | VERIFY | 每页 title/description、OG、favicon、robots/sitemap 有明确策略 | 内容确认 | 已有 metadata、robots、sitemap;favicon/逐页文案待确认 |
|
||||
| T021 | 分析和隐私策略 | DEFER | 若添加 analytics,Privacy 同步说明并记录供应商 | T016 | 当前未添加 analytics |
|
||||
| T022 | 工程化拆分 | DONE | 从单文件拆为可维护结构,或明确保持单文件的理由 | T002-T011 | 已新增 Next.js App Router 工程,保留 doc 基线 |
|
||||
| T023 | 自动化测试 | TODO | 至少覆盖核心路由冒烟、表单提交、未知路由回退 | T022 | 当前无测试框架 |
|
||||
| T024 | 部署到云服务器 8082 | TODO | 服务器 `127.0.0.1:8082` 返回 200,systemd 或等价进程可自恢复 | T022 或静态服务 | 目标主机 170.106.192.152 |
|
||||
| T025 | HTTPS 域名发布 | BLOCKED | `https://paprikalaw.com/` 返回 200,证书有效 | DNS/服务器权限/T024 | 需确认 DNS 指向 |
|
||||
| T026 | 截图/PDF 验收资产 | TODO | 核心页面截图或 PDF 存档,路径写入 `05-验收证据.md` | T018 | 用于回归比较 |
|
||||
| T027 | `blog` 和 `about` 去留确认 | TODO | 明确保留、隐藏或删除;导航和 footer 一致 | 产品输入 | 当前代码存在但未公开 |
|
||||
| T028 | 完全对标补强 working 文档 | DONE | 01-06 和 README 覆盖页面、开发步骤、台账、任务矩阵、证据、决策 | T001/T002 | R002 |
|
||||
| T029 | 修复 Services 中不存在的 `contact` 路由 | DONE | Services 04/05/06 CTA 指向存在页面,点击不回退首页 | T006 | 已改 `start`,Next `/services` 产物确认 href `/start` |
|
||||
| T030 | 服务器凭据安全处理 | TODO | 不在仓库保存明文密码;配置 SSH key 或受控密钥交接 | T024 | 用户已提供临时凭据 |
|
||||
| T031 | Outlet/network 陈述风险确认 | BLOCKED | 负责人确认可展示 outlet/network 名称或改成示例类型 | 产品/法务输入 | Press/Judging 涉及名称 |
|
||||
| T032 | README 索引维护 | DONE | README 能指向 01-06,并说明后续入口 | T028 | 本轮更新 |
|
||||
| T033 | Git 仓库状态确认 | TODO | 明确项目是否应纳入 Git;若是,初始化或进入正确仓库后记录 commit hash | 用户/仓库输入 | 当前 `git status` 失败 |
|
||||
| T034 | 源码级对标附录完善 | DONE | 文档覆盖 CSS token、组件类、函数清单、CTA 映射、FAQ 编号、Legal 结构、核验命令 | T028 | R003 |
|
||||
| T035 | 明确正式技术架构 | DONE | 文档明确采用 Next.js + React + PostgreSQL,并记录 ORM、路由、API、部署方式 | 用户确认 | R004 |
|
||||
| T036 | 初始化 Next.js + React 工程 | DONE | `npm run dev`、`npm run build` 可用,基础页面路由建立 | T035 | TypeScript + App Router,Next 16 + React 19 |
|
||||
| T037 | PostgreSQL + Prisma 接入 | VERIFY | `schema.prisma`、`.env.example`、migration 存在,能连接数据库 | T035/T040 | 本地代码完成;真实连接待生产 `DATABASE_URL` |
|
||||
| T038 | 表单入库实现 | VERIFY | `/start`、`/ask` 提交写入 PostgreSQL,并返回 `submission_id` | T036/T037/T044 | API 写入代码完成;缺真实 DB 成功提交证据 |
|
||||
| T039 | Next.js 部署到 8082 | VERIFY | `npm run start -- -p 8082` 生产可运行,systemd 管理 | T036/T037 | 本地生产 8082 通过;远程 systemd 未做 |
|
||||
| T040 | 远程验证 PostgreSQL 安装 | TODO | 服务器上 `systemctl status postgresql` 正常,`psql --version` 可用,应用数据库/用户确认 | 用户已确认安装 | 不记录明文密码 |
|
||||
| T041 | 邮箱账号创建/确认 | TODO | admin 入口可登录,10 个普通邮箱可通过 webmail 登录 | 邮件后台权限 | 不记录统一初始密码 |
|
||||
| T042 | 邮件 DNS 与收发验证 | TODO | MX/SPF/DKIM/DMARC 检查完成,内外部收发测试通过 | T041/DNS 权限 | 记录 dig 输出和测试截图 |
|
||||
| T043 | 表单邮件通知配置 | TODO | `/start`、`/ask` 入库后向指定邮箱发送通知,记录 message id | T038/T041 | SMTP 密码放环境变量 |
|
||||
| T044 | 真实 PostgreSQL 入库验收 | TODO | 配置 `DATABASE_URL` 后,Start/Ask 成功返回 `submission_id`,数据库可查记录 | T037/T040 | 本地未配置数据库,远程待执行 migration |
|
||||
|
||||
## 优先级建议
|
||||
|
||||
| 优先级 | 任务 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| P0 | T029 | 明确死链,改动小,影响转化 |
|
||||
| P0 | T017/T016 | 法务日期和文本影响正式发布可信度 |
|
||||
| P0 | T012/T013/T014 | 当前无法真实收集线索 |
|
||||
| P0 | T035/T036/T037/T038 | 正式架构已明确,需落地工程和数据库 |
|
||||
| P0 | T040 | PostgreSQL 已安装,需验证后才能接 Prisma |
|
||||
| P0 | T041/T042 | 公司邮箱是对外联系和表单通知基础设施 |
|
||||
| P1 | T003-T011/T018/T026 | 页面上线前必须有截图证据 |
|
||||
| P1 | T024/T025/T030 | 服务器发布和凭据安全 |
|
||||
| P2 | T020/T021/T023 | SEO、analytics、自动化测试 |
|
||||
| P2 | T022 | 工程化拆分取决于上线节奏 |
|
||||
|
||||
## 防重复规则
|
||||
|
||||
1. 开始任务前先查本表编号。
|
||||
2. 已是 `DONE` 的任务不得重复做,除非新建变更任务并说明原因。
|
||||
3. `VERIFY` 任务完成验证后改为 `DONE`,并在 `05-验收证据.md` 填证据。
|
||||
4. 涉及法务、隐私、表单数据、部署凭据的任务必须有明确验收证据,不能只凭页面看起来正常关闭。
|
||||
5. 每次新增任务必须使用下一个编号,并补齐状态、验收标准和依赖。
|
||||
374
working/05-验收证据.md
Normal file
374
working/05-验收证据.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# 05-验收证据
|
||||
|
||||
本文件记录可复查证据:命令、页面、job_id、report_id、PDF、截图、部署链接、提交记录等。
|
||||
|
||||
## E001 - 文档基线盘点
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T001、T002
|
||||
- 类型:命令输出摘要
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
pwd
|
||||
rg --files -g '!*node_modules*' -g '!*.png' -g '!*.jpg' -g '!*.jpeg' -g '!*.gif' -g '!*.pdf' | sed -n '1,160p'
|
||||
sed -n '1,260p' doc/index.html
|
||||
find working -maxdepth 2 -type f -print | sort
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- 当前工作目录为 `/home/mes123456/MeetPaprika`。
|
||||
- 参考文件存在:`doc/index.html`。
|
||||
- `working` 目录存在。
|
||||
- `doc/index.html` 包含 `<main id="app"></main>`、内联 CSS、内联 JS。
|
||||
- 页面通过 `?page=...` 查询参数选择内容。
|
||||
|
||||
## E002 - 当前路由证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T002
|
||||
- 类型:源码证据
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
rg -n "const pages|function|data-page|render|page:|title:|href=|main.innerHTML|case|pages\\[|\\?page=|<body|<script|</html>|Services|Resources|FAQ|Privacy|Terms|Get Started|Application|Contact" doc/index.html
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- 导航显式链接:`press`、`judging`、`scholarly`、`services`、`why`、`resources`、`faq`、`start`。
|
||||
- Footer 链接:`why`、`faq`、`terms`、`privacy`、`disclaimer`。
|
||||
- `pageData` 中定义:`home`、`press`、`judging`、`scholarly`、`services`、`why`、`resources`、`faq`、`blog`、`start`、`ask`、`thanks`、`about`、`terms`、`privacy`、`disclaimer`。
|
||||
- 路由选择逻辑:未识别页面回退为 `home`。
|
||||
|
||||
## E003 - 表单 mockup 证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T012、T013、T014、T015
|
||||
- 类型:源码证据
|
||||
- 文件:`doc/index.html`
|
||||
- 结果摘要:
|
||||
- `start` 页面字段使用 `<div class="field">`,提交入口是 `<a class="button primary" href="?page=thanks">Submit Profile</a>`。
|
||||
- `ask` 页面字段使用 `<div class="field">`,提交入口是 `<a class="button primary" href="?page=thanks">Send Question</a>`。
|
||||
- 当前没有 `<form>`、`input`、`textarea`、`file`、`method`、`action` 或提交接口。
|
||||
- 因此当前只能证明视觉流程存在,不能证明真实提交能力。
|
||||
|
||||
## E004 - R002 源码复核证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T028
|
||||
- 类型:命令输出摘要
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
sed -n '2262,2468p' doc/index.html
|
||||
sed -n '2469,2765p' doc/index.html
|
||||
sed -n '2766,3428p' doc/index.html
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- 复核 header、footer、`serviceCards`、`pageData`、`hero`、`heroWithKicker`、`pressPage`、`judgingPage`、`o1CriteriaPage`、`faqPage`、legal 页面和最终渲染逻辑。
|
||||
- `document.getElementById("app").innerHTML = pageData[selected].body;`
|
||||
- `if (["faq", "resources"].includes(selected))` 会高亮 Resources trigger。
|
||||
- legal 页面当时日期写为 `Last updated: July 10, 2026`;后续已在 E024 修复。
|
||||
|
||||
## E005 - Services contact 路由缺陷证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T029
|
||||
- 类型:源码证据
|
||||
- 文件:`doc/index.html`
|
||||
- 结果摘要:
|
||||
- Services 页面卡片 04、05、06 调用:
|
||||
- `card("04", ..., "Start Here", "contact")`
|
||||
- `card("05", ..., "Start Here", "contact")`
|
||||
- `card("06", ..., "Start Here", "contact")`
|
||||
- `pageData` 没有 `contact`。
|
||||
- 当前路由逻辑会把未知 `contact` 回退到 `home`。
|
||||
- 结论:这是明确死链/误跳转缺陷;后续已在 E024 修复。
|
||||
|
||||
## E006 - Git 状态证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T033
|
||||
- 类型:命令输出摘要
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- 输出:`fatal: not a git repository (or any of the parent directories): .git`
|
||||
- 结论:当前路径不是 Git 仓库,暂时无法记录 commit hash。
|
||||
|
||||
## E007 - Working 文档更新证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T028、T032
|
||||
- 类型:文件证据
|
||||
- 文件:
|
||||
- `working/README.md`
|
||||
- `working/01-项目功能内容.md`
|
||||
- `working/02-项目程序开发详细步骤.md`
|
||||
- `working/03-推进台账.md`
|
||||
- `working/04-任务矩阵.md`
|
||||
- `working/05-验收证据.md`
|
||||
- `working/06-决策记录.md`
|
||||
- 结果摘要:
|
||||
- 已形成 README 索引。
|
||||
- 已形成从项目功能、开发步骤、推进台账、任务矩阵、验收证据到决策记录的闭环。
|
||||
|
||||
## E016 - 源码级对标扫描证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T034
|
||||
- 类型:命令输出摘要
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
wc -l doc/index.html working/*.md
|
||||
rg -n 'function [A-Za-z0-9_]+\(|const [A-Za-z0-9_]+ =|^[[:space:]]+[A-Za-z0-9_]+: \{|href="\?page=|Last updated|Packages start|@media|--[a-z-]+:' doc/index.html
|
||||
rg -n '^\s*\.[A-Za-z0-9_-]+|^\s*#[A-Za-z0-9_-]+|^\s*[a-z]+\s*\{|@media|grid-template-columns|width: min|border-radius|background:' doc/index.html
|
||||
sed -n '2870,3035p' doc/index.html
|
||||
sed -n '3070,3228p' doc/index.html
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- `doc/index.html` 共 3423 行。
|
||||
- CSS token 包含 `--red`、`--deep-red`、`--ink`、`--text`、`--muted`、`--line`、`--paper`、`--panel`、`--green`、`--header-text`。
|
||||
- 唯一显式断点为 `@media (max-width: 1080px)`。
|
||||
- `pageData` 包含 `home`、`press`、`judging`、`scholarly`、`services`、`why`、`resources`、`faq`、`blog`、`start`、`ask`、`thanks`、`about`、`terms`、`privacy`、`disclaimer`。
|
||||
- FAQ 共 4 组 20 个问题。
|
||||
- Terms 共 18 个 section,但编号缺少 17。
|
||||
- Privacy 共 12 个 section。
|
||||
- Disclaimer 共 9 个 section。
|
||||
- Legal 页面当时日期均为 `Last updated: July 10, 2026`;后续已在 E024 修复。
|
||||
- 发现未调用函数/模块:`productPage`、`productPageWithKicker`、`judgingNetworkGroup`、`resourcesHub`、`resourceGroup`、`legalPage`、`standardGrid`、`pressQuality`。
|
||||
|
||||
## E017 - 技术架构确认记录
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T035
|
||||
- 类型:用户确认 + 文档证据
|
||||
- 用户确认:
|
||||
|
||||
```text
|
||||
Next+ React ,数据库使用 PostGreSQL
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- 正式开发技术架构已收敛为 Next.js + React + PostgreSQL。
|
||||
- 文档默认 ORM 为 Prisma。
|
||||
- 表单提交目标改为 Next.js API/Server Actions 写入 PostgreSQL。
|
||||
- 生产启动方式改为 `npm run start -- -p 8082`。
|
||||
- 新增任务 T036-T039,覆盖工程初始化、数据库接入、表单入库、Next.js 部署。
|
||||
|
||||
## E018 - 服务器 PostgreSQL 安装用户确认
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T040
|
||||
- 类型:用户确认
|
||||
- 用户确认摘要:
|
||||
- PostgreSQL 已在云服务器安装完成。
|
||||
- 服务器:`170.106.192.152`
|
||||
- SSH 用户:`ubuntu`
|
||||
- 可用端口:`8080-8099`
|
||||
- 应用目标端口:`8082`
|
||||
- 凭据处理:
|
||||
- 用户在对话中提供了 SSH 密码。
|
||||
- 长期文档不保存明文密码。
|
||||
- 待验证命令:
|
||||
|
||||
```bash
|
||||
ssh ubuntu@170.106.192.152
|
||||
systemctl status postgresql --no-pager
|
||||
psql --version
|
||||
sudo -u postgres psql -c '\l'
|
||||
```
|
||||
|
||||
- 结论:
|
||||
- 当前状态为“用户确认已安装,待远程验证”。
|
||||
|
||||
## E019 - 邮箱系统账号需求用户确认
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T041、T042、T043
|
||||
- 类型:用户确认
|
||||
- 用户确认摘要:
|
||||
- 普通用户入口:`https://mail.paprikalaw.com/webmail/`
|
||||
- 管理员入口:`https://mail.paprikalaw.com/admin/`
|
||||
- 管理员账号:`admin@paprikalaw.com`
|
||||
- 普通邮箱账号:
|
||||
- `info@paprikalaw.com`
|
||||
- `contact@paprikalaw.com`
|
||||
- `support@paprikalaw.com`
|
||||
- `sales@paprikalaw.com`
|
||||
- `marketing@paprikalaw.com`
|
||||
- `hr@paprikalaw.com`
|
||||
- `finance@paprikalaw.com`
|
||||
- `billing@paprikalaw.com`
|
||||
- `legal@paprikalaw.com`
|
||||
- `service@paprikalaw.com`
|
||||
- 凭据处理:
|
||||
- 用户在对话中提供了统一初始密码。
|
||||
- 长期文档不保存明文密码。
|
||||
- 待验证:
|
||||
- 管理员登录。
|
||||
- 普通邮箱 webmail 登录。
|
||||
- MX/SPF/DKIM/DMARC。
|
||||
- 内部互发、外部收信、外部发信。
|
||||
- Next.js 表单通知 SMTP 配置。
|
||||
|
||||
## E022 - Next.js 工程初始化和构建证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T036、T022
|
||||
- 类型:命令输出摘要
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run generate:legacy
|
||||
npm run lint
|
||||
npx tsc --noEmit
|
||||
npm run build
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- 依赖安装完成。
|
||||
- `generate:legacy` 从 `doc/index.html` 生成 12 个页面数据。
|
||||
- `lint` 通过。
|
||||
- TypeScript `--noEmit` 通过。
|
||||
- `next build` 通过,生成 `/`、`/[page]`、`/api/submissions`、`/ask`、`/start`、`/thanks`、`/robots.txt`、`/sitemap.xml`。
|
||||
|
||||
## E023 - Prisma schema 和 migration 证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T037
|
||||
- 类型:文件和命令证据
|
||||
- 文件:
|
||||
- `prisma/schema.prisma`
|
||||
- `prisma/migrations/20260706120000_init/migration.sql`
|
||||
- `.env.example`
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
npm run prisma:generate
|
||||
npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- Prisma Client 生成成功。
|
||||
- schema 包含 `Submission` 和 `SubmissionFile`。
|
||||
- `.env.example` 只保留占位 `DATABASE_URL`,未写入真实密码。
|
||||
- 待补:
|
||||
- 生产 PostgreSQL 执行 `npx prisma migrate deploy`。
|
||||
- 成功入库后记录 `submission_id`。
|
||||
|
||||
## E024 - Services CTA 和 legal 日期修复证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T017、T029
|
||||
- 类型:源码和 HTTP 产物证据
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
rg -n "contact|Last updated" doc/index.html
|
||||
curl -s http://localhost:8082/services | rg -n 'href="/start"|Expert consultation|Attorney Referral'
|
||||
curl -s http://localhost:8082/terms | rg -n "Last updated: July 6, 2026"
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- `doc/index.html` 中 Services 04/05/06 已不再指向不存在的 `contact`。
|
||||
- `/services` 产物中 Expert consultation、Agency representation、Attorney Referral 的 CTA 均为 `href="/start"`。
|
||||
- legal 页面显示 `Last updated: July 6, 2026`。
|
||||
|
||||
## E025 - 本地生产 8082 路由冒烟证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T003-T011、T015、T020、T039
|
||||
- 类型:HTTP 状态码证据
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
npm run start -- -p 8082
|
||||
for p in / /press /judging /scholarly /services /why /resources /faq /start /ask /thanks /terms /privacy /disclaimer /robots.txt /sitemap.xml; do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8082$p)
|
||||
printf '%s %s\n' "$code" "$p"
|
||||
done
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- 所有列出的路径均返回 `200`。
|
||||
- `robots.txt` 输出 `Sitemap: https://paprikalaw.com/sitemap.xml`。
|
||||
- `sitemap.xml` 包含核心公开路径。
|
||||
- 备注:
|
||||
- 这是本地生产服务验收,不等同于远程服务器和 HTTPS 验收。
|
||||
|
||||
## E026 - 表单 API 校验证据
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T012、T013、T038、T044
|
||||
- 类型:API 响应证据
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8082/api/submissions \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"type":"ask","name":"","email":"bad","question":"","consentAccepted":false}'
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- 返回 `Validation failed`。
|
||||
- 字段错误包含 `name`、`email`、`consentAccepted`、`question`。
|
||||
- 待补:
|
||||
- 配置 `DATABASE_URL` 后提交有效 `/start` 和 `/ask` 表单,记录 `submission_id` 和数据库查询结果。
|
||||
- 未配置 `DATABASE_URL` 时,有效提交会返回 500,不能作为入库成功证据。
|
||||
|
||||
## E027 - npm audit 结果
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 关联任务:T023、T036
|
||||
- 类型:安全扫描摘要
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
npm audit --audit-level=moderate --json
|
||||
```
|
||||
|
||||
- 结果摘要:
|
||||
- 剩余 2 个 `moderate` vulnerability。
|
||||
- 来源为 `next` 依赖链中的 `postcss <8.5.10` 公告。
|
||||
- 当前项目已使用 registry 当前稳定版本 Next `16.2.10`。
|
||||
- npm 给出的 `fixAvailable` 会降级到 `next@9.3.3`,属于不合理破坏性修复,未执行。
|
||||
- 后续:
|
||||
- 等待 Next/PostCSS 依赖链发布可用修复版本后升级。
|
||||
|
||||
## 待补证据清单
|
||||
|
||||
| 编号 | 关联任务 | 需要补充的证据 |
|
||||
| --- | --- | --- |
|
||||
| E008 | T003-T011 | 桌面浏览器截图,覆盖 home、press、judging、scholarly、services、why、resources、faq |
|
||||
| E009 | T018 | 移动端截图,至少 390px 宽度覆盖 home、services、faq、start |
|
||||
| E010 | T012/T013 | 表单提交成功记录、submission id、后台记录截图或 webhook job_id |
|
||||
| E011 | T016/T017 | 法务审核记录、确认人、确认日期 |
|
||||
| E012 | T023 | 自动化测试命令和结果 |
|
||||
| E013 | T024/T025 | 部署 URL、systemd 状态、Nginx 测试、证书、job_id、commit hash |
|
||||
| E014 | T026 | PDF 或截图归档路径 |
|
||||
| E015 | T030 | SSH key 或密钥管理方式确认,不能包含明文密码 |
|
||||
| E020 | T041/T042 | 邮箱后台截图、DNS 查询输出、收发测试截图 |
|
||||
| E021 | T043 | 表单通知邮件 message id 或测试截图 |
|
||||
| E028 | T044 | PostgreSQL `DATABASE_URL` 配置后的真实表单提交、`submission_id` 和数据库查询结果 |
|
||||
|
||||
## 证据模板
|
||||
|
||||
### EXXX - 标题
|
||||
|
||||
- 日期:
|
||||
- 关联任务:
|
||||
- 类型:
|
||||
- 命令或页面:
|
||||
- 结果:
|
||||
- 文件或链接:
|
||||
- job_id/report_id/submission_id:
|
||||
- 备注:
|
||||
195
working/06-决策记录.md
Normal file
195
working/06-决策记录.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 06-决策记录
|
||||
|
||||
本文件记录已经做出的工程、产品和流程决策。后续若要改变决策,需要新增记录说明原因,不直接覆盖旧记录。
|
||||
|
||||
## D001 - 当前基线按静态单文件 mockup 记录
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:当前以 `doc/index.html` 作为项目基线,而不是假设已有完整前端工程。
|
||||
- 原因:
|
||||
- 当前目录未发现 `package.json`、构建配置或后端代码。
|
||||
- `doc/index.html` 已包含页面、样式和路由脚本。
|
||||
- 用户本轮要求是在 `working` 中编写推进文档。
|
||||
- 影响:
|
||||
- 当前验收以静态页面可打开、路由可渲染、视觉可检查为主。
|
||||
- 表单、部署、测试、工程化都作为后续任务单独推进。
|
||||
|
||||
## D002 - 表单不按真实功能验收
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:`start` 和 `ask` 当前只能记为表单视觉 mockup,不能记为真实线索收集功能。
|
||||
- 原因:
|
||||
- 字段是 `<div>`,不是输入控件。
|
||||
- 提交按钮是跳转链接,不会提交数据。
|
||||
- 没有后端、第三方表单服务、文件上传或校验逻辑。
|
||||
- 影响:
|
||||
- `T012`、`T013`、`T014` 保持 `TODO`。
|
||||
- 上线前必须实现真实提交,或在页面明确标注不可提交。
|
||||
|
||||
## D003 - 任务推进以任务矩阵为唯一入口
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:后续开发先查 `04-任务矩阵.md`,每个任务必须有编号、状态、验收标准和依赖。
|
||||
- 原因:
|
||||
- 用户明确要求防止重复做。
|
||||
- 当前项目范围包含内容、前端、表单、法务、部署和证据留存,容易混线。
|
||||
- 影响:
|
||||
- 新工作不得只写在聊天记录里。
|
||||
- 完成任务必须同步推进台账和验收证据。
|
||||
|
||||
## D004 - 法务内容按草稿处理
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:Terms、Privacy、Disclaimer 当前按草稿记录,正式上线前需要法务或负责人确认。
|
||||
- 原因:
|
||||
- 页面涉及 non-law-firm statement、attorney-client relationship、refund、privacy、file upload、attorney referral、immigration outcome guarantee 等高风险内容。
|
||||
- 页面曾使用 `Last updated: July 10, 2026`,晚于当前日期 `2026-07-06`;R007 已修正日期,但内容仍需审核。
|
||||
- 影响:
|
||||
- `T016` 标为 `BLOCKED`,等待法务/负责人输入。
|
||||
- `T017` 后续已完成日期修正;法务内容审核仍不关闭。
|
||||
|
||||
## D005 - 本轮不改 doc/index.html
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:R002 只补强 `working` 文档,不修改 `doc/index.html`。
|
||||
- 原因:
|
||||
- 用户要求是在 `working` 中编写文档体系。
|
||||
- 当前还没有要求开始代码修复或工程化迁移。
|
||||
- 影响:
|
||||
- `T029` 虽已识别,但仍为 `TODO`。
|
||||
- 后续修复死链时应单独开一轮并记录证据。
|
||||
|
||||
## D006 - 长期文档不保存明文服务器密码
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:用户提供的 SSH 密码只视为临时交接信息,不写入 `working` 长期文档。
|
||||
- 原因:
|
||||
- `working` 文档会作为后续推进资料,可能被复制、上传或纳入版本控制。
|
||||
- 明文密码进入项目文件会扩大泄露面。
|
||||
- 影响:
|
||||
- 服务器信息保留 host、user、port、domain。
|
||||
- `T030` 要求改用 SSH key、部署密钥或受控密钥库。
|
||||
|
||||
## D007 - 目标部署端口采用 8082
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:部署方案默认内部服务监听 `8082`,HTTPS 由域名和反向代理对外提供。
|
||||
- 原因:
|
||||
- 用户明确给出端口号 `8082`。
|
||||
- 用户说明 `8080-8099` 已开放。
|
||||
- HTTPS 通常由 Nginx/Caddy/负载均衡终止,再转发到本地应用端口。
|
||||
- 影响:
|
||||
- `T024` 验收服务器本地 `127.0.0.1:8082`。
|
||||
- `T025` 验收 `https://paprikalaw.com/`。
|
||||
|
||||
## D008 - Services 的 contact 路由判定为缺陷
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:Services 页面 04/05/06 CTA 指向 `contact` 是缺陷,不按设计接受。
|
||||
- 原因:
|
||||
- `pageData` 没有 `contact` 页面。
|
||||
- 当前路由逻辑会把未知页面回退到 `home`。
|
||||
- CTA 文案是 Start Here,合理目标应为 `start`,除非新增真实 Contact 页面。
|
||||
- 影响:
|
||||
- 新增 `T029`。
|
||||
- 修复前 `T006` 只能保持 `VERIFY`,不能标为 `DONE`。
|
||||
|
||||
## D009 - Outlet/network 名称正式发布前需确认
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:Press 和 Judging 页面中的 outlet/network 名称上线前必须由负责人确认,或改成示例类型表达。
|
||||
- 原因:
|
||||
- 页面列出大量媒体和 judging/network 名称,可能被理解为合作关系、可保证 placement 或背书。
|
||||
- 这类陈述涉及商业真实性和法律风险。
|
||||
- 影响:
|
||||
- 新增 `T031`,状态 `BLOCKED`。
|
||||
- 未确认前不建议直接正式上线。
|
||||
|
||||
## D010 - 完全对标必须包含视觉和源码结构
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:后续提到“完全对标 `doc/index.html`”时,不能只记录页面文案,还必须覆盖 CSS token、组件类、函数清单、CTA 映射、FAQ 编号、Legal section、未调用模块和源码缺陷。
|
||||
- 原因:
|
||||
- `doc/index.html` 是 3423 行单文件 mockup,页面内容、视觉规范和路由逻辑耦合在一起。
|
||||
- 如果只记录页面功能,工程化拆分时容易丢失视觉细节或保留隐藏缺陷。
|
||||
- 影响:
|
||||
- `01-项目功能内容.md` 增加源码级对标附录。
|
||||
- `02-项目程序开发详细步骤.md` 增加完全对标核验清单。
|
||||
- 新增 `T034` 和 `E016`。
|
||||
|
||||
## D011 - 正式开发架构采用 Next.js + React + PostgreSQL
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:正式开发技术架构采用 Next.js + React,数据库使用 PostgreSQL。
|
||||
- 默认补充决策:
|
||||
- 使用 TypeScript。
|
||||
- 使用 Next.js App Router。
|
||||
- 使用 Prisma 作为 PostgreSQL ORM。
|
||||
- 使用 Next.js Route Handlers 或 Server Actions 处理表单提交。
|
||||
- 生产服务监听 `8082`,HTTPS 由反向代理处理。
|
||||
- 原因:
|
||||
- 用户明确指定 `Next+ React` 和 `PostGreSQL`。
|
||||
- 项目需要真实表单、数据持久化、后续可能的文件上传和后台线索管理,单纯静态站不够。
|
||||
- Next.js 可同时承接页面、API、SEO、构建和生产服务。
|
||||
- 影响:
|
||||
- `02-项目程序开发详细步骤.md` 中工程化路线改为 Next.js + PostgreSQL。
|
||||
- 新增 `T035-T039`。
|
||||
- 后续不再优先推进 Vite/纯静态拆分,除非另有新决策。
|
||||
|
||||
## D012 - PostgreSQL 已安装但仍需远程验证
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:将服务器 PostgreSQL 状态记录为“用户确认已安装,待远程验证”,不直接视为已完成数据库接入。
|
||||
- 原因:
|
||||
- 用户已明确说明 PostgreSQL 在服务器安装完成。
|
||||
- 但尚未执行服务状态、版本、数据库列表、应用数据库、应用用户和连接字符串验证。
|
||||
- 影响:
|
||||
- 新增 `T040` 远程验证 PostgreSQL 安装。
|
||||
- `T037` PostgreSQL + Prisma 接入仍保持 `TODO`。
|
||||
- 继续执行不保存明文密码的凭据规则。
|
||||
|
||||
## D013 - 邮箱账号统一记录但不保存初始密码
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:记录 `paprikalaw.com` 邮箱入口、管理员账号和 10 个普通邮箱账号,但不在长期文档保存用户提供的统一初始密码。
|
||||
- 原因:
|
||||
- 邮箱账号属于项目基础设施,网站联系、表单通知和法务联系需要引用。
|
||||
- 多账号共用初始密码存在安全风险,进入文档会扩大泄露面。
|
||||
- 影响:
|
||||
- 新增 `T041-T043`。
|
||||
- 创建或确认邮箱后,应尽快改为独立强密码并记录验证证据。
|
||||
- Next.js SMTP 凭据只能放服务器环境变量或受控密钥库。
|
||||
|
||||
## D014 - 保留 doc/index.html 作为对标基线,同时以 Next.js 为正式工程
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:不删除 `doc/index.html`,正式代码在根目录 Next.js 工程中维护。
|
||||
- 原因:
|
||||
- `doc/index.html` 是既有视觉、文案和路由逻辑的完整基线。
|
||||
- 新工程需要持续对标旧 mockup,避免内容迁移漂移。
|
||||
- `scripts/extract-legacy-pages.mjs` 可从旧基线重新生成静态页面 CSS 和 HTML 数据。
|
||||
- 影响:
|
||||
- 静态内容页面暂由 `lib/legacy-pages.ts` 和 `LegacyPage` 渲染。
|
||||
- `/start`、`/ask`、`/thanks` 已由 React 页面接管,不再使用旧 mockup 表单。
|
||||
|
||||
## D015 - 文件上传暂不启用
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:Start/Ask 表单当前不启用 CV/resume 上传。
|
||||
- 原因:
|
||||
- 文件上传涉及存储位置、大小限制、访问权限、隐私保留期限和删除策略。
|
||||
- 未确认前收集 CV 会扩大敏感数据风险。
|
||||
- 影响:
|
||||
- `SubmissionFile` 表作为未来能力预留。
|
||||
- 前端表单明确说明 file upload 暂停,直到存储和隐私方案获批。
|
||||
|
||||
## D016 - 表单入库任务不在缺少 DATABASE_URL 时关闭
|
||||
|
||||
- 日期:2026-07-06
|
||||
- 决策:虽然 `/api/submissions` 已实现 Prisma 写入代码,但在没有真实 PostgreSQL `DATABASE_URL` 和成功 `submission_id` 证据前,T038/T044 不标为 `DONE`。
|
||||
- 原因:
|
||||
- API 代码通过构建不等于数据已经成功持久化。
|
||||
- 生产数据库连接、migration、权限和网络策略都可能导致运行时失败。
|
||||
- 影响:
|
||||
- T012/T013/T038 保持 `VERIFY`。
|
||||
- 新增 T044,专门记录真实 PostgreSQL 入库验收。
|
||||
54
working/README.md
Normal file
54
working/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# MeetPaprika Working Index
|
||||
|
||||
本目录用于承接 Paprika 网站后续开发推进。所有后续工作优先从 `04-任务矩阵.md` 领取任务,并在完成后同步更新 `03-推进台账.md`、`05-验收证据.md` 和必要的 `06-决策记录.md`。
|
||||
|
||||
## 文档索引
|
||||
|
||||
1. [01-项目功能内容.md](01-项目功能内容.md)
|
||||
对标 `/home/mes123456/MeetPaprika/doc/index.html`,说明当前 Paprika 静态网站 mockup 的页面、功能、内容模块、交互方式、视觉规范、CSS 类、JS 函数、CTA 映射、表单边界、服务器目标和已知风险。
|
||||
|
||||
2. [02-项目程序开发详细步骤.md](02-项目程序开发详细步骤.md)
|
||||
说明从当前单文件 mockup 到可维护、可上线版本的开发步骤,包括静态修复、真实表单、工程化拆分、8082 部署、HTTPS、回归验收和完全对标核验命令。
|
||||
|
||||
3. [03-推进台账.md](03-推进台账.md)
|
||||
记录每轮做了什么、改了哪些文件、验证了什么、下一步是什么。
|
||||
|
||||
4. [04-任务矩阵.md](04-任务矩阵.md)
|
||||
统一维护任务编号、状态、验收标准和依赖,防止重复做同一件事。
|
||||
|
||||
5. [05-验收证据.md](05-验收证据.md)
|
||||
记录命令、页面、截图、PDF、job_id、report_id、submission_id、部署链接等证据。
|
||||
|
||||
6. [06-决策记录.md](06-决策记录.md)
|
||||
记录关键设计、工程、部署和安全决策,后续变更时先查这里,避免反复争论同一个问题。
|
||||
|
||||
## 当前基线
|
||||
|
||||
- 参考文件:`/home/mes123456/MeetPaprika/doc/index.html`
|
||||
- 当前形态:已新增 Next.js + React 工程;`doc/index.html` 保留为静态 mockup 对标基线。
|
||||
- 静态基线:`doc/index.html` 仍通过查询参数 `?page=...` 渲染不同页面。
|
||||
- Next 路由:`/`、`/press`、`/judging`、`/scholarly`、`/services`、`/why`、`/resources`、`/faq`、`/start`、`/ask`、`/thanks`、`/terms`、`/privacy`、`/disclaimer`。
|
||||
- 当前表单:`/start` 和 `/ask` 已改为真实 React 表单,提交到 `POST /api/submissions`;成功入库依赖生产 `DATABASE_URL`。
|
||||
- 数据访问层:Prisma ORM;已存在 `prisma/schema.prisma` 和初始化 migration。
|
||||
- 目标域名:`https://paprikalaw.com/`
|
||||
- 目标服务器:`ubuntu@170.106.192.152`
|
||||
- 目标端口:`8082`
|
||||
- 邮箱普通入口:`https://mail.paprikalaw.com/webmail/`
|
||||
- 邮箱管理员入口:`https://mail.paprikalaw.com/admin/`
|
||||
- 凭据规则:长期文档不保存明文密码,后续部署改用 SSH key 或受控密钥交接。
|
||||
|
||||
## 下一步建议
|
||||
|
||||
1. 配置生产 `.env` 的 `DATABASE_URL`,在服务器 PostgreSQL 上执行 Prisma migration。
|
||||
2. 远程验证 `170.106.192.152` 的 PostgreSQL、应用数据库和应用用户。
|
||||
3. 将 Next.js 应用部署到服务器 `8082`,再配置 HTTPS 反向代理到 `https://paprikalaw.com/`。
|
||||
4. 登录邮件后台确认邮箱、DNS 和收发,再接入表单通知。
|
||||
5. 完成法务文本、outlet/network 名称、文件上传策略和截图验收。
|
||||
|
||||
## 工作规则
|
||||
|
||||
1. 开始前查 `04-任务矩阵.md`。
|
||||
2. 完成后写 `03-推进台账.md`。
|
||||
3. 有命令、截图、部署、表单提交、PDF、job_id、report_id 时写 `05-验收证据.md`。
|
||||
4. 有路线、合规、安全、部署、架构取舍时写 `06-决策记录.md`。
|
||||
5. 不把 SSH 密码、API key、表单 webhook secret、CRM token 写入本目录。
|
||||
Reference in New Issue
Block a user