first commit
This commit is contained in:
712
design_handoff_helder_workbench/design/src/data.js
Normal file
712
design_handoff_helder_workbench/design/src/data.js
Normal file
@@ -0,0 +1,712 @@
|
||||
/* Mock project: filesystem tree, file contents, before/after pairs, runtime diff. */
|
||||
(function () {
|
||||
// ---- working-tree (current / updated) file contents ----------------
|
||||
const F = {};
|
||||
|
||||
F["src/Http/Controller/UserController.php"] = `<?php
|
||||
|
||||
namespace App\\Http\\Controller;
|
||||
|
||||
use App\\Service\\PaymentService;
|
||||
use App\\Repository\\UserRepository;
|
||||
use Psr\\Http\\Message\\ResponseInterface;
|
||||
use Psr\\Http\\Message\\ServerRequestInterface;
|
||||
|
||||
final class UserController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserRepository $users,
|
||||
private readonly PaymentService $payments,
|
||||
) {}
|
||||
|
||||
public function show(ServerRequestInterface $request, string $id): ResponseInterface
|
||||
{
|
||||
$user = $this->users->find($id);
|
||||
|
||||
if ($user === null) {
|
||||
return $this->json(['error' => 'user_not_found'], 404);
|
||||
}
|
||||
|
||||
$balance = $this->payments->balanceFor($user);
|
||||
|
||||
return $this->json([
|
||||
'id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'plan' => $user->plan->value,
|
||||
'name' => $user->name,
|
||||
'currency' => $user->currency,
|
||||
'balance' => $balance->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(ServerRequestInterface $request, string $id): ResponseInterface
|
||||
{
|
||||
$user = $this->users->find($id);
|
||||
if ($user === null) {
|
||||
return $this->json(['error' => 'user_not_found'], 404);
|
||||
}
|
||||
|
||||
$data = (array) $request->getParsedBody();
|
||||
$user->fill($this->onlyFillable($data));
|
||||
$this->users->save($user);
|
||||
|
||||
return $this->json($user->toArray());
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function onlyFillable(array $data): array
|
||||
{
|
||||
$allowed = ['email', 'plan', 'name'];
|
||||
return array_intersect_key($data, array_flip($allowed));
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
F["src/Service/PaymentService.php"] = `<?php
|
||||
|
||||
namespace App\\Service;
|
||||
|
||||
use App\\Entity\\User;
|
||||
use App\\ValueObject\\Money;
|
||||
use App\\Gateway\\PaymentGateway;
|
||||
use Psr\\Log\\LoggerInterface;
|
||||
|
||||
final class PaymentService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PaymentGateway $gateway,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function balanceFor(User $user): Money
|
||||
{
|
||||
$cents = $this->gateway->lookupBalance($user->id);
|
||||
return Money::fromCents($cents, $user->currency ?? 'EUR');
|
||||
}
|
||||
|
||||
public function charge(User $user, Money $amount, string $reason): bool
|
||||
{
|
||||
if ($amount->isZero()) {
|
||||
$this->logger->warning('Skipped zero charge', ['user' => $user->id]);
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->gateway->charge($user->paymentToken, $amount->cents());
|
||||
$this->logger->info('Charge attempt', [
|
||||
'user' => $user->id,
|
||||
'amount' => $amount->cents(),
|
||||
'ok' => $result->success,
|
||||
]);
|
||||
|
||||
return $result->success;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
F["public/assets/app.js"] = `import { createStore } from './store.js';
|
||||
import { mountRouter } from './router.js';
|
||||
|
||||
const store = createStore({
|
||||
user: null,
|
||||
notifications: [],
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
async function bootstrap() {
|
||||
const res = await fetch('/api/session', { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const session = await res.json();
|
||||
store.set('user', session.user);
|
||||
store.set('theme', session.user.theme ?? 'dark');
|
||||
}
|
||||
|
||||
mountRouter(document.querySelector('#app'), store);
|
||||
store.subscribe('notifications', renderToasts);
|
||||
}
|
||||
|
||||
function renderToasts(list) {
|
||||
const host = document.querySelector('#toasts');
|
||||
host.replaceChildren(...list.map((n) => {
|
||||
const el = document.createElement('div');
|
||||
el.className = \\\`toast toast--\\\${n.level}\\\`;
|
||||
el.textContent = n.message;
|
||||
return el;
|
||||
}));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', bootstrap);
|
||||
`;
|
||||
|
||||
F["public/assets/store.js"] = `export function createStore(initial = {}) {
|
||||
let state = { ...initial };
|
||||
const subs = new Map();
|
||||
|
||||
return {
|
||||
get: (key) => state[key],
|
||||
set(key, value) {
|
||||
state = { ...state, [key]: value };
|
||||
(subs.get(key) || []).forEach((fn) => fn(value, state));
|
||||
},
|
||||
subscribe(key, fn) {
|
||||
const list = subs.get(key) || [];
|
||||
list.push(fn);
|
||||
subs.set(key, list);
|
||||
return () => subs.set(key, list.filter((f) => f !== fn));
|
||||
},
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
F["public/assets/styles.css"] = `:root {
|
||||
--brand: #4d8dff;
|
||||
--ink: #15171a;
|
||||
--paper: #ffffff;
|
||||
--radius: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
background: var(--ink);
|
||||
color: #e6e8ea;
|
||||
}
|
||||
|
||||
.toast {
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius);
|
||||
border-left: 3px solid var(--brand);
|
||||
}
|
||||
|
||||
.toast--error { border-left-color: #e0696a; }
|
||||
.toast--success { border-left-color: #5cbd6b; }
|
||||
`;
|
||||
|
||||
F["src/types/api.ts"] = `export type Plan = 'free' | 'pro' | 'enterprise';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
plan: Plan;
|
||||
currency: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Balance {
|
||||
cents: number;
|
||||
currency: string;
|
||||
formatted: string;
|
||||
}
|
||||
|
||||
export type ApiResult<T> =
|
||||
| { ok: true; data: T }
|
||||
| { ok: false; error: string; status: number };
|
||||
|
||||
export async function getUser(id: string): Promise<ApiResult<User>> {
|
||||
const res = await fetch(\\\`/api/users/\\\${id}\\\`);
|
||||
if (!res.ok) {
|
||||
return { ok: false, error: 'request_failed', status: res.status };
|
||||
}
|
||||
return { ok: true, data: (await res.json()) as User };
|
||||
}
|
||||
`;
|
||||
|
||||
F["scripts/migrate.py"] = `#!/usr/bin/env python3
|
||||
"""Run pending database migrations in order."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from db import connect, applied_migrations
|
||||
|
||||
MIGRATIONS = Path(__file__).parent / "migrations"
|
||||
|
||||
|
||||
def pending(conn):
|
||||
done = applied_migrations(conn)
|
||||
files = sorted(MIGRATIONS.glob("*.sql"))
|
||||
return [f for f in files if f.stem not in done]
|
||||
|
||||
|
||||
def run(conn, migration: Path) -> None:
|
||||
sql = migration.read_text()
|
||||
print(f" -> applying {migration.stem}")
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql)
|
||||
cur.execute(
|
||||
"INSERT INTO schema_migrations (version) VALUES (%s)",
|
||||
(migration.stem,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
conn = connect()
|
||||
todo = pending(conn)
|
||||
if not todo:
|
||||
print("Database is up to date.")
|
||||
return 0
|
||||
print(f"Applying {len(todo)} migration(s)...")
|
||||
for migration in todo:
|
||||
run(conn, migration)
|
||||
print("Done.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
`;
|
||||
|
||||
F["scripts/seed.py"] = `#!/usr/bin/env python3
|
||||
"""Seed the database with demo data for local development."""
|
||||
import random
|
||||
from db import connect
|
||||
|
||||
PLANS = ["free", "pro", "enterprise"]
|
||||
|
||||
|
||||
def seed_users(conn, count: int = 25) -> None:
|
||||
with conn.cursor() as cur:
|
||||
for i in range(count):
|
||||
cur.execute(
|
||||
"INSERT INTO users (email, plan) VALUES (%s, %s)",
|
||||
(f"user{i}@example.com", random.choice(PLANS)),
|
||||
)
|
||||
conn.commit()
|
||||
print(f"Seeded {count} users.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_users(connect())
|
||||
`;
|
||||
|
||||
F["templates/dashboard.html"] = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dashboard</title>
|
||||
<link rel="stylesheet" href="/assets/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app" class="layout">
|
||||
<header class="topbar">
|
||||
<h1 class="logo">Console</h1>
|
||||
<nav class="nav">
|
||||
<a href="/users" class="nav__link">Users</a>
|
||||
<a href="/billing" class="nav__link">Billing</a>
|
||||
</nav>
|
||||
</header>
|
||||
<section id="content" class="content"></section>
|
||||
</main>
|
||||
<div id="toasts" class="toast-host"></div>
|
||||
<script type="module" src="/assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
F["config/app.json"] = `{
|
||||
"name": "console",
|
||||
"env": "production",
|
||||
"features": {
|
||||
"billing": true,
|
||||
"newDashboard": true,
|
||||
"exportCsv": false
|
||||
},
|
||||
"payment": {
|
||||
"gateway": "stripe",
|
||||
"currency": "EUR",
|
||||
"retryLimit": 3
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"channel": "stdout"
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
F["composer.json"] = `{
|
||||
"name": "blijnder/console",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"psr/log": "^3.0",
|
||||
"psr/http-message": "^2.0",
|
||||
"nyholm/psr7": "^1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"phpstan/phpstan": "^1.11"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "App\\\\": "src/" }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
F["package.json"] = `{
|
||||
"name": "console-frontend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.3.0",
|
||||
"vitest": "^2.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
F["README.md"] = `# Console
|
||||
|
||||
Internal admin console. PHP API + small vanilla JS frontend.
|
||||
|
||||
## Getting started
|
||||
|
||||
composer install
|
||||
npm install
|
||||
python scripts/migrate.py
|
||||
npm run dev
|
||||
|
||||
## Layout
|
||||
|
||||
- \`src/\` PHP application code (PSR-4, \`App\\\` namespace)
|
||||
- \`public/\` Document root and frontend assets
|
||||
- \`scripts/\` Python maintenance + migration scripts
|
||||
- \`templates/\` Server-rendered HTML
|
||||
`;
|
||||
|
||||
F[".env"] = `APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
DATABASE_URL=postgres://localhost:5432/console
|
||||
PAYMENT_GATEWAY=stripe
|
||||
PAYMENT_CURRENCY=EUR
|
||||
LOG_LEVEL=info
|
||||
`;
|
||||
|
||||
// ---- ORIGINAL (pre-edit) versions of changed files ----------------
|
||||
const O = {};
|
||||
|
||||
O["src/Http/Controller/UserController.php"] = `<?php
|
||||
|
||||
namespace App\\Http\\Controller;
|
||||
|
||||
use App\\Service\\PaymentService;
|
||||
use App\\Repository\\UserRepository;
|
||||
use Psr\\Http\\Message\\ResponseInterface;
|
||||
use Psr\\Http\\Message\\ServerRequestInterface;
|
||||
|
||||
final class UserController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserRepository $users,
|
||||
private readonly PaymentService $payments,
|
||||
) {}
|
||||
|
||||
public function show(ServerRequestInterface $request, string $id): ResponseInterface
|
||||
{
|
||||
$user = $this->users->find($id);
|
||||
|
||||
if ($user === null) {
|
||||
return $this->json(['error' => 'user_not_found'], 404);
|
||||
}
|
||||
|
||||
$balance = $this->payments->balanceFor($user);
|
||||
|
||||
return $this->json([
|
||||
'id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'plan' => $user->plan,
|
||||
'balance' => $balance->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(ServerRequestInterface $request, string $id): ResponseInterface
|
||||
{
|
||||
$user = $this->users->find($id);
|
||||
if ($user === null) {
|
||||
return $this->json(['error' => 'user_not_found'], 404);
|
||||
}
|
||||
|
||||
$data = (array) $request->getParsedBody();
|
||||
$user->fill($this->onlyFillable($data));
|
||||
$this->users->save($user);
|
||||
|
||||
return $this->json($user->toArray());
|
||||
}
|
||||
|
||||
private function onlyFillable(array $data): array
|
||||
{
|
||||
return array_intersect_key($data, array_flip(['email', 'plan']));
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
O["public/assets/app.js"] = `import { createStore } from './store.js';
|
||||
import { mountRouter } from './router.js';
|
||||
|
||||
const store = createStore({
|
||||
user: null,
|
||||
notifications: [],
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
async function bootstrap() {
|
||||
const res = await fetch('/api/session');
|
||||
if (res.ok) {
|
||||
const session = await res.json();
|
||||
store.set('user', session.user);
|
||||
}
|
||||
|
||||
mountRouter(document.querySelector('#app'), store);
|
||||
store.subscribe('notifications', renderToasts);
|
||||
}
|
||||
|
||||
function renderToasts(list) {
|
||||
const host = document.querySelector('#toasts');
|
||||
host.replaceChildren(...list.map((n) => {
|
||||
const el = document.createElement('div');
|
||||
el.className = \\\`toast toast--\\\${n.level}\\\`;
|
||||
el.textContent = n.message;
|
||||
return el;
|
||||
}));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', bootstrap);
|
||||
`;
|
||||
|
||||
O["config/app.json"] = `{
|
||||
"name": "console",
|
||||
"env": "production",
|
||||
"features": {
|
||||
"billing": true,
|
||||
"newDashboard": false
|
||||
},
|
||||
"payment": {
|
||||
"gateway": "stripe",
|
||||
"currency": "EUR",
|
||||
"retryLimit": 3
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"channel": "stdout"
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
O["scripts/migrate.py"] = `#!/usr/bin/env python3
|
||||
"""Run pending database migrations in order."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from db import connect, applied_migrations
|
||||
|
||||
MIGRATIONS = Path(__file__).parent / "migrations"
|
||||
|
||||
|
||||
def pending(conn):
|
||||
done = applied_migrations(conn)
|
||||
files = sorted(MIGRATIONS.glob("*.sql"))
|
||||
return [f for f in files if f.stem not in done]
|
||||
|
||||
|
||||
def run(conn, migration: Path) -> None:
|
||||
sql = migration.read_text()
|
||||
print(f" -> applying {migration.stem}")
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql)
|
||||
cur.execute(
|
||||
"INSERT INTO schema_migrations (version) VALUES (%s)",
|
||||
(migration.stem,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
conn = connect()
|
||||
todo = pending(conn)
|
||||
if not todo:
|
||||
print("Database is up to date.")
|
||||
return 0
|
||||
for migration in todo:
|
||||
run(conn, migration)
|
||||
print("Done.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
`;
|
||||
|
||||
// PaymentService is a brand-new file (added) -> original is empty
|
||||
O["src/Service/PaymentService.php"] = "";
|
||||
|
||||
// LegacyUser was deleted -> original content, no working-tree version
|
||||
O["src/Model/LegacyUser.php"] = `<?php
|
||||
|
||||
namespace App\\Model;
|
||||
|
||||
/**
|
||||
* @deprecated Superseded by App\\Entity\\User. Kept only for the
|
||||
* legacy billing import; safe to remove once the importer is gone.
|
||||
*/
|
||||
final class LegacyUser
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int $id,
|
||||
public readonly string $email,
|
||||
public readonly ?string $plan = null,
|
||||
) {}
|
||||
|
||||
public static function fromRow(array $row): self
|
||||
{
|
||||
return new self(
|
||||
(int) $row['id'],
|
||||
(string) $row['email'],
|
||||
$row['plan'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'email' => $this->email,
|
||||
'plan' => $this->plan,
|
||||
];
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ---- file tree (nested) -------------------------------------------
|
||||
const tree = {
|
||||
name: "console", type: "dir", path: "", open: true, children: [
|
||||
{ name: "config", type: "dir", path: "config", open: false, children: [
|
||||
{ name: "app.json", type: "file", path: "config/app.json" },
|
||||
]},
|
||||
{ name: "public", type: "dir", path: "public", open: true, children: [
|
||||
{ name: "assets", type: "dir", path: "public/assets", open: true, children: [
|
||||
{ name: "app.js", type: "file", path: "public/assets/app.js" },
|
||||
{ name: "store.js", type: "file", path: "public/assets/store.js" },
|
||||
{ name: "styles.css", type: "file", path: "public/assets/styles.css" },
|
||||
]},
|
||||
]},
|
||||
{ name: "scripts", type: "dir", path: "scripts", open: false, children: [
|
||||
{ name: "migrate.py", type: "file", path: "scripts/migrate.py" },
|
||||
{ name: "seed.py", type: "file", path: "scripts/seed.py" },
|
||||
]},
|
||||
{ name: "src", type: "dir", path: "src", open: true, children: [
|
||||
{ name: "Http", type: "dir", path: "src/Http", open: true, children: [
|
||||
{ name: "Controller", type: "dir", path: "src/Http/Controller", open: true, children: [
|
||||
{ name: "UserController.php", type: "file", path: "src/Http/Controller/UserController.php" },
|
||||
]},
|
||||
]},
|
||||
{ name: "Service", type: "dir", path: "src/Service", open: true, children: [
|
||||
{ name: "PaymentService.php", type: "file", path: "src/Service/PaymentService.php" },
|
||||
]},
|
||||
{ name: "types", type: "dir", path: "src/types", open: false, children: [
|
||||
{ name: "api.ts", type: "file", path: "src/types/api.ts" },
|
||||
]},
|
||||
]},
|
||||
{ name: "templates", type: "dir", path: "templates", open: false, children: [
|
||||
{ name: "dashboard.html", type: "file", path: "templates/dashboard.html" },
|
||||
]},
|
||||
{ name: ".env", type: "file", path: ".env" },
|
||||
{ name: "composer.json", type: "file", path: "composer.json" },
|
||||
{ name: "package.json", type: "file", path: "package.json" },
|
||||
{ name: "README.md", type: "file", path: "README.md" },
|
||||
],
|
||||
};
|
||||
|
||||
// ---- line-based LCS diff ------------------------------------------
|
||||
function buildDiff(origText, updText) {
|
||||
const a = origText === "" ? [] : origText.replace(/\n$/, "").split("\n");
|
||||
const b = updText === "" ? [] : updText.replace(/\n$/, "").split("\n");
|
||||
const n = a.length, m = b.length;
|
||||
const dp = Array.from({ length: n + 1 }, () => new Int32Array(m + 1));
|
||||
for (let i = n - 1; i >= 0; i--)
|
||||
for (let j = m - 1; j >= 0; j--)
|
||||
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
||||
|
||||
const ops = [];
|
||||
let i = 0, j = 0;
|
||||
while (i < n && j < m) {
|
||||
if (a[i] === b[j]) { ops.push({ t: "same", a: i, b: j }); i++; j++; }
|
||||
else if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push({ t: "del", a: i }); i++; }
|
||||
else { ops.push({ t: "add", b: j }); j++; }
|
||||
}
|
||||
while (i < n) { ops.push({ t: "del", a: i++ }); }
|
||||
while (j < m) { ops.push({ t: "add", b: j++ }); }
|
||||
|
||||
const rows = [], left = [], right = [], split = [];
|
||||
const delSet = new Set(), addSet = new Set();
|
||||
let add = 0, del = 0;
|
||||
for (const op of ops) {
|
||||
if (op.t === "same") {
|
||||
rows.push({ sign: " ", oldNo: op.a + 1, newNo: op.b + 1, text: a[op.a] });
|
||||
} else if (op.t === "del") {
|
||||
rows.push({ sign: "-", oldNo: op.a + 1, newNo: null, text: a[op.a] });
|
||||
delSet.add(op.a); del++;
|
||||
} else {
|
||||
rows.push({ sign: "+", oldNo: null, newNo: op.b + 1, text: b[op.b] });
|
||||
addSet.add(op.b); add++;
|
||||
}
|
||||
}
|
||||
a.forEach((text, idx) => left.push({ no: idx + 1, text, mark: delSet.has(idx) ? "del" : null }));
|
||||
b.forEach((text, idx) => right.push({ no: idx + 1, text, mark: addSet.has(idx) ? "add" : null }));
|
||||
|
||||
// aligned split rows (pair del/add blocks)
|
||||
let dbuf = [], abuf = [];
|
||||
const flush = () => {
|
||||
const k = Math.max(dbuf.length, abuf.length);
|
||||
for (let x = 0; x < k; x++) split.push({ l: dbuf[x] || null, r: abuf[x] || null });
|
||||
dbuf = []; abuf = [];
|
||||
};
|
||||
for (const op of ops) {
|
||||
if (op.t === "same") { flush(); split.push({ l: { no: op.a + 1, text: a[op.a] }, r: { no: op.b + 1, text: b[op.b] } }); }
|
||||
else if (op.t === "del") dbuf.push({ no: op.a + 1, text: a[op.a], mark: "del" });
|
||||
else abuf.push({ no: op.b + 1, text: b[op.b], mark: "add" });
|
||||
}
|
||||
flush();
|
||||
|
||||
return { rows, left, right, split, add, del };
|
||||
}
|
||||
|
||||
// ---- changed files -------------------------------------------------
|
||||
const changeDefs = [
|
||||
{ path: "src/Service/PaymentService.php", status: "A" },
|
||||
{ path: "src/Http/Controller/UserController.php", status: "M" },
|
||||
{ path: "public/assets/app.js", status: "M" },
|
||||
{ path: "config/app.json", status: "M" },
|
||||
{ path: "scripts/migrate.py", status: "M" },
|
||||
{ path: "src/Model/LegacyUser.php", status: "D" },
|
||||
];
|
||||
|
||||
const diffs = {};
|
||||
const changes = changeDefs.map((c) => {
|
||||
const orig = O[c.path] != null ? O[c.path] : "";
|
||||
const upd = F[c.path] != null ? F[c.path] : "";
|
||||
const d = buildDiff(orig, upd);
|
||||
diffs[c.path] = Object.assign(d, {
|
||||
deleted: c.status === "D",
|
||||
added: c.status === "A",
|
||||
original: orig,
|
||||
updated: upd,
|
||||
});
|
||||
return { path: c.path, status: c.status, add: d.add, del: d.del, deleted: c.status === "D" };
|
||||
});
|
||||
|
||||
window.PROJECT = {
|
||||
name: "console",
|
||||
branch: "feat/payments-balance",
|
||||
files: F,
|
||||
originals: O,
|
||||
tree,
|
||||
diffs,
|
||||
changes,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user