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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pr-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Install Dependencies
run: npm ci --legacy-peer-deps

- name: Mock Portfolio Template Library for CI
run: node scripts/mock-template-library.mjs

- name: Linting & Formatting
run: |
npm run lint
Expand Down
18 changes: 18 additions & 0 deletions .hallmark/log.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"date": "2026-05-30",
"scope": "app",
"macrostructure": "Readable portfolio tour + Tabbed studio",
"theme": "VeriWorkly platform",
"enrichment": "Interactive full-template preview",
"brief": "Corrective portfolio makeover: readable landing page, compact studio, complete billing decision page, populated demos, and retuned templates."
},
{
"date": "2026-05-30",
"scope": "app",
"macrostructure": "Editorial product tour + Workbench",
"theme": "VeriWorkly platform",
"enrichment": "Tier-A CSS grid surface",
"brief": "Full production makeover for the portfolio builder, editor, billing view, gallery, and public templates."
}
]
2 changes: 1 addition & 1 deletion apps/blog-platform/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@veriworkly/blog-platform",
"version": "3.11.0",
"version": "3.12.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs-platform/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@veriworkly/docs-platform",
"version": "3.11.0",
"version": "3.12.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
2 changes: 2 additions & 0 deletions apps/portfolio/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
NEXT_PUBLIC_BACKEND_URL=http://localhost:8080/api/v1
BACKEND_INTERNAL_URL=http://localhost:8080/api/v1
51 changes: 51 additions & 0 deletions apps/portfolio/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# except example file
!.env.example
!.env.docker.example

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# fumadocs generated files
.source

# testing
/app/og-generator
41 changes: 41 additions & 0 deletions apps/portfolio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# VeriWorkly Portfolio

The public portfolio platform. Private template implementations are mounted as a Git submodule at `template-library/`.

## Setup

Clone with the private templates:

```bash
git clone --recurse-submodules git@github.com:VeriWorkly/veriworkly.git
```

For an existing checkout:

```bash
git submodule update --init --recursive
```

The private repository uses GitHub SSH access. The machine running development or deployment must trust GitHub's SSH host key and have access to `VeriWorkly/portfolio-templates`.

## Add A Template

1. Add a folder in `template-library/` with its own React component and optional scoped stylesheet.
2. Add one dynamic loader entry in `template-library/registry.ts`.
3. Add public gallery metadata in `templates/catalog/templates.ts`.
4. Commit and push the private repository first.
5. Commit the updated submodule pointer in this repository.

Do not import template styles from `app/globals.css`. Template modules own their styles so Next.js can emit per-template assets.

## Production Deployment

Portfolio publishing requires:

1. Point `portfolio.veriworkly.com` and `*.veriworkly.com` at the portfolio Next.js deployment.
2. Provision TLS coverage for `portfolio.veriworkly.com` and `*.veriworkly.com`.
3. Configure `NEXT_PUBLIC_BACKEND_URL` and `BACKEND_INTERNAL_URL`.
4. Configure the server Dodo Payments and Cloudflare R2 variables documented in `apps/server/.env.example`.
5. Set the auth cookie domain to `.veriworkly.com` so Studio and Portfolio share the signed-in session.

Only VeriWorkly subdomains are supported at launch. Custom-domain routing and certificate automation are intentionally out of scope.
26 changes: 26 additions & 0 deletions apps/portfolio/app/api/render/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";

import { parsePortfolioContent } from "@/lib/portfolio";

import { renderTemplate } from "@/templates/runtime/registry";

export async function POST(request: Request) {
try {
const project = parsePortfolioContent(await request.json());

const element = await renderTemplate(project);

const { renderToString } = await import("react-dom/server");
const html = renderToString(element);

return new NextResponse(html, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store, max-age=0",
},
});
} catch (error) {
console.error("Failed to render preview template:", error);
return new NextResponse("Error rendering template", { status: 500 });
}
}
41 changes: 41 additions & 0 deletions apps/portfolio/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { revalidatePath, revalidateTag } from "next/cache";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
let body: { paths?: string[]; tags?: string[]; secret?: string };

try {
body = (await request.json()) as { paths?: string[]; tags?: string[]; secret?: string };
} catch {
return NextResponse.json({ message: "Invalid JSON body" }, { status: 400 });
}

const { paths, tags, secret } = body;
const expectedSecret = process.env.PORTFOLIO_REVALIDATE_SECRET || "dev-revalidate-secret";

if (secret !== expectedSecret) {
return NextResponse.json({ message: "Invalid secret" }, { status: 401 });
}

if (tags && Array.isArray(tags)) {
for (const tag of tags) {
try {
(revalidateTag as unknown as (tag: string) => void)(tag);
} catch (err) {
console.error(`Failed to revalidate tag: ${tag}`, err);
}
}
}

if (paths && Array.isArray(paths)) {
for (const path of paths) {
try {
revalidatePath(path);
} catch (err) {
console.error(`Failed to revalidate path: ${path}`, err);
}
}
}

return NextResponse.json({ revalidated: true, paths, tags });
}
10 changes: 10 additions & 0 deletions apps/portfolio/app/billing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Metadata } from "next";
import { BillingWorkspace } from "@/components/BillingWorkspace";

export const metadata: Metadata = {
title: "Portfolio Pro billing",
robots: { index: false, follow: false },
};
export default function BillingPage() {
return <BillingWorkspace />;
}
7 changes: 7 additions & 0 deletions apps/portfolio/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Metadata } from "next";
import { DashboardWorkspace } from "@/components/DashboardWorkspace";

export const metadata: Metadata = { title: "Dashboard", robots: { index: false, follow: false } };
export default function DashboardPage() {
return <DashboardWorkspace />;
}
24 changes: 24 additions & 0 deletions apps/portfolio/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

export default function ErrorPage({ reset }: { reset: () => void }) {
return (
<main className="grid min-h-screen place-items-center bg-[var(--color-paper)] p-6 text-center">
<div className="max-w-md rounded-xl border border-[var(--color-line)] bg-[var(--color-panel)] p-7">
<p className="text-[11px] font-extrabold tracking-[.14em] text-[var(--color-accent)] uppercase">
Temporary issue
</p>
<h1 className="mt-3 text-3xl font-bold tracking-[-.06em]">This page could not load.</h1>
<p className="mt-3 text-sm leading-6 text-[var(--color-muted)]">
Your work has not been lost. Try the request again, or return after the service connection
recovers.
</p>
<button
className="mt-5 inline-flex min-h-10 items-center rounded-lg bg-[var(--color-accent)] px-4 text-xs font-extrabold text-[var(--color-accent-ink)]"
onClick={reset}
>
Try again
</button>
</div>
</main>
);
}
96 changes: 96 additions & 0 deletions apps/portfolio/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* Hallmark · pre-emit critique: P5 H5 E4 S5 R5 V5
* genre: modern-minimal · macrostructure: Editorial product tour + Workbench · tone: structured confidence · anchor hue: blue
* contrast: pass (46-50) · nav: N1 · footer: Ft1 · tokens: pass (58) · mobile: pass (36, 59, 61-69)
*/
@import "tailwindcss";
@import "./tokens.css";

* {
box-sizing: border-box;
}
html,
body {
margin: 0;
overflow-x: clip;
scroll-behavior: smooth;
}
body {
background: var(--color-paper);
color: var(--color-ink);
font-family: var(--font-body);
}
.surface-grid {
background-image:
linear-gradient(to right, var(--color-line) 1px, transparent 1px),
linear-gradient(to bottom, var(--color-line) 1px, transparent 1px);
background-size: 28px 28px;
}
.paper-noise {
background:
radial-gradient(circle at 10% 0%, var(--color-accent-soft), transparent 26rem),
var(--color-paper);
}
.reveal {
animation: reveal var(--dur-slow) var(--ease-out) both;
}
.reveal-delay {
animation-delay: 120ms;
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
textarea {
font: inherit;
}
button,
a {
-webkit-tap-highlight-color: transparent;
}
button:focus-visible,
a:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: 3px;
}
button:active,
a:active {
opacity: 0.72;
}
button:disabled,
input:disabled,
textarea:disabled {
cursor: not-allowed;
opacity: 0.55;
}
h1,
h2 {
min-width: 0;
overflow-wrap: anywhere;
}
@media (prefers-reduced-motion: reduce) {
html,
body {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
transition-duration: 1ms !important;
}
}
Loading
Loading