Skip to content
Draft
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ scratch/
.gocache/
.gotmp/
.gomodcache/

# JS workspaces
node_modules/
js/**/dist/
js/**/.vite/
js/**/coverage/
js/**/playwright-report/
js/**/test-results/
12 changes: 12 additions & 0 deletions js/reviews-example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@amika/reviews · example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions js/reviews-example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@amika/reviews-example",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 4173",
"typecheck": "tsc --noEmit",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@amika/reviews": "workspace:*",
"react": "^19.2.3",
"react-dom": "^19.2.3"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.3.0",
"typescript": "^5",
"vite": "^5.4.0"
}
}
25 changes: 25 additions & 0 deletions js/reviews-example/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
testDir: "./tests",
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? "github" : "list",
use: {
baseURL: "http://localhost:5173",
trace: "on-first-retry",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
],
webServer: {
command: "pnpm run dev",
url: "http://localhost:5173",
reuseExistingServer: !process.env.CI,
timeout: 30_000,
stdout: "pipe",
stderr: "pipe",
},
});
24 changes: 24 additions & 0 deletions js/reviews-example/public/fixtures/multi-file.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
diff --git a/src/a.ts b/src/a.ts
index 0000001..0000002 100644
--- a/src/a.ts
+++ b/src/a.ts
@@ -1,2 +1,2 @@
-export const A = 1;
+export const A = 2;
export const ALWAYS = true;
diff --git a/src/b.ts b/src/b.ts
new file mode 100644
index 0000000..0000003
--- /dev/null
+++ b/src/b.ts
@@ -0,0 +1,2 @@
+export const B = 1;
+export const NEW = true;
diff --git a/src/c.ts b/src/c.ts
deleted file mode 100644
index 0000004..0000000
--- a/src/c.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const C = 1;
-export const OLD = true;
12 changes: 12 additions & 0 deletions js/reviews-example/public/fixtures/rename.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
diff --git a/src/old-name.ts b/src/new-name.ts
similarity index 80%
rename from src/old-name.ts
rename to src/new-name.ts
index 0000001..0000002 100644
--- a/src/old-name.ts
+++ b/src/new-name.ts
@@ -1,3 +1,3 @@
export function thing() {
- return 1;
+ return 2;
}
10 changes: 10 additions & 0 deletions js/reviews-example/public/fixtures/simple.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
diff --git a/src/hello.ts b/src/hello.ts
index 0000001..0000002 100644
--- a/src/hello.ts
+++ b/src/hello.ts
@@ -1,3 +1,4 @@
export function hello(name: string) {
- return `Hello, ${name}`;
+ return `Hello, ${name}!`;
+ // greeting was made friendlier
}
176 changes: 176 additions & 0 deletions js/reviews-example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { useEffect, useState } from "react";
import {
CodeReview,
ReviewProvider,
useReview,
usePatches,
useReviewState,
} from "@amika/reviews";
import type { ReviewItem } from "@amika/reviews";

const FIXTURES = [
"/fixtures/simple.patch",
"/fixtures/multi-file.patch",
"/fixtures/rename.patch",
];

export function App() {
const url = new URL(window.location.href);
const persistKey = url.searchParams.get("persistKey") ?? undefined;
const author = url.searchParams.get("author") ?? "demo-user";

return (
<ReviewProvider
persistKey={persistKey}
author={author}
linkSync
>
<AppInner />
</ReviewProvider>
);
}

function AppInner() {
const store = useReview();
return (
<>
<Toolbar />
<CodeReview store={store} />
</>
);
}

function patchLabel(item: ReviewItem): string {
if (item.label) return item.label;
if (item.kind === "patch") {
const match = item.patchText.match(/^Subject: (?:\[PATCH[^\]]*\] )?(.+)$/m);
if (match) return match[1].trim();
}
return item.id.slice(0, 8);
}

function PatchNavigator() {
const store = useReview();
const items = usePatches();
const state = useReviewState();

if (items.length === 0) return null;

const currentIndex = items.findIndex(
(i) => i.id === state.selection.itemId,
);
const displayIndex = currentIndex === -1 ? 0 : currentIndex;
const current = items[displayIndex];

function go(index: number) {
const target = items[index];
const files = store.listFiles(target.id);
if (files.length > 0) {
store.selectFile(files[0].itemId, files[0].path);
} else {
store.selectFile(target.id, null);
}
}

return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 6,
fontSize: 13,
}}
>
<button
type="button"
onClick={() => go(displayIndex - 1)}
disabled={displayIndex === 0}
aria-label="Previous patch"
>
</button>
<span
title={patchLabel(current)}
style={{
maxWidth: 260,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
color: "#24292f",
}}
>
{patchLabel(current)}{" "}
<span style={{ color: "#57606a" }}>
({displayIndex + 1}/{items.length})
</span>
</span>
<button
type="button"
onClick={() => go(displayIndex + 1)}
disabled={displayIndex === items.length - 1}
aria-label="Next patch"
>
</button>
</div>
);
}

function Toolbar() {
const store = useReview();
const [loading, setLoading] = useState(false);

async function loadFixtures() {
setLoading(true);
try {
const texts = await Promise.all(
FIXTURES.map((u) => fetch(u).then((r) => r.text())),
);
store.loadFromText(texts);
} finally {
setLoading(false);
}
}

useEffect(() => {
// Optional auto-load via ?fixtures=1 for Playwright convenience.
const url = new URL(window.location.href);
if (url.searchParams.get("fixtures") === "1") {
void loadFixtures();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div
style={{
padding: 8,
borderBottom: "1px solid #d0d7de",
display: "flex",
gap: 8,
alignItems: "center",
}}
data-amika-example="toolbar"
>
<button
type="button"
onClick={loadFixtures}
disabled={loading}
data-amika-example="load-fixtures"
>
{loading ? "Loading…" : "Load fixture series"}
</button>
<button
type="button"
onClick={() => store.reset()}
data-amika-example="reset"
>
Reset
</button>
<PatchNavigator />
<span style={{ marginLeft: "auto", color: "#57606a" }}>
@amika/reviews · example
</span>
</div>
);
}
13 changes: 13 additions & 0 deletions js/reviews-example/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import "@amika/reviews/styles.css";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App.js";

const root = document.getElementById("root");
if (!root) throw new Error("root not found");

createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
);
Loading
Loading