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
76 changes: 47 additions & 29 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
name: Claude Code Review
name: PR Review with Progress Tracking

# This example demonstrates how to use the track_progress feature to get
# visual progress tracking for PR reviews, similar to v0.x agent mode.

on:
pull_request:
types: [opened, synchronize]
types: [opened, synchronize, ready_for_review, reopened]

jobs:
claude-review:
review-with-tracking:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 1

Expand All @@ -23,28 +25,44 @@ jobs:
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Enable progress tracking
track_progress: true

# Your custom review instructions
prompt: |
Review this pull request with focus on this codebase's specific patterns:

## Code Quality
- tRPC routers: correct procedure type (publicProcedure vs authenticatedProcedure vs staffProcedure), proper Zod input validation, class-based model usage
- Kysely queries: safe query construction, correct usage of dual databases (graphDB for app data, federatedDB for blockchain data via FDW)
- TypeScript strict mode compliance (noUncheckedIndexedAccess — check for unchecked indexed access)
- Coding conventions: `~/` path alias, inline type imports, kebab-case files, const object enums (not TS enum)

## Architecture
- Server/Client Component split: unnecessary "use client" directives, data fetching in Server Components via `caller`, props passed to `*Client` components
- Caching: proper use of cacheQuery with TTL and tag-based invalidation, cache key correctness

## Security
- SIWE authentication: proper session checks, RBAC permission enforcement via hasPermission()
- Blockchain interactions: address validation with Viem, safe smart contract calls, no hardcoded private keys or sensitive data
- Input validation: Zod schemas on all tRPC inputs, no unsanitized user input in queries

## Performance
- Avoid N+1 queries in Kysely models, prefer batch fetches
- Proper use of React Suspense boundaries and streaming
- Check for unnecessary re-renders in client components

Be constructive and specific. Reference the relevant file and line when possible.
claude_args: "--max-turns 10"
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}

Perform a comprehensive code review with the following focus areas:

1. **Code Quality**
- Clean code principles and best practices
- Proper error handling and edge cases
- Code readability and maintainability

2. **Security**
- Check for potential security vulnerabilities
- Validate input sanitization
- Review authentication/authorization logic

3. **Performance**
- Identify potential performance bottlenecks
- Review database queries for efficiency
- Check for memory leaks or resource issues

4. **Testing**
- Verify adequate test coverage
- Review test quality and edge cases
- Check for missing test scenarios

5. **Documentation**
- Ensure code is properly documented
- Verify README updates for new features
- Check API documentation accuracy

Provide detailed feedback using inline comments for specific issues.
Use top-level comments for general observations or praise.

# Tools for comprehensive PR review
claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
36 changes: 34 additions & 2 deletions src/app/(main)/pools/[address]/pool-client-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import { TagIcon } from "lucide-react";
import Image from "next/image";
import { useParams } from "next/navigation";
import { toast } from "sonner";
import { getAddress } from "viem";
import Address from "~/components/address";
import { EditableImageOverlay } from "~/components/editable-image-overlay";
import { ContentContainer } from "~/components/layout/content-container";
import { useSwapPool } from "~/components/pools/hooks";
import { Badge } from "~/components/ui/badge";
import { useAuth } from "~/hooks/use-auth";
import { useIsContractOwner } from "~/hooks/use-is-owner";
import { trpc } from "~/lib/trpc";
import { celoscanUrl } from "~/utils/celo";
import { hasPermission } from "~/utils/permissions";
import { PoolButtons } from "./pool-buttons-client";
import { PoolTabs } from "./pool-tabs";

Expand All @@ -21,10 +25,38 @@ export function PoolClientPage() {
const { data: metadata } = trpc.pool.get.useQuery(pool_address);

const isOwner = useIsContractOwner(pool_address);
const auth = useAuth();
const utils = trpc.useUtils();
const updatePool = trpc.pool.update.useMutation();

const canEdit = hasPermission(auth?.user, isOwner, "Pools", "UPDATE");

const handleBannerSave = async (url: string) => {
try {
await updatePool.mutateAsync({
pool_address: pool_address,
banner_url: url,
});
await utils.pool.get.refetch(pool_address);
toast.success("Banner updated");
} catch (error) {
console.error(error);
toast.error("Failed to update banner");
}
};

return (
<ContentContainer title={metadata?.pool_name ?? pool?.name ?? ""} className="bg-transparent">
{/* Modern Hero Section */}
<div className="relative overflow-hidden rounded-2xl shadow-2xl">
<EditableImageOverlay
canEdit={canEdit}
folder="pools"
aspectRatio={16 / 9}
onImageSaved={handleBannerSave}
isSaving={updatePool.isPending}
overlayPosition="top-right"
className="overflow-hidden rounded-2xl shadow-2xl"
>
{/* Banner Background */}
{metadata?.banner_url ? (
<div className="absolute inset-0">
Expand Down Expand Up @@ -142,7 +174,7 @@ export function PoolClientPage() {
{/* Decorative Elements */}
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-white/10 to-transparent rounded-full blur-3xl" />
<div className="absolute bottom-0 left-0 w-48 h-48 bg-gradient-to-tr from-white/5 to-transparent rounded-full blur-2xl" />
</div>
</EditableImageOverlay>

{/* Modern Tabs Section */}
<div className="mt-12">
Expand Down
171 changes: 171 additions & 0 deletions src/components/editable-image-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"use client";

import { CameraIcon } from "lucide-react";
import React, { useRef, useState } from "react";
import type Webcam from "react-webcam";
import { toast } from "sonner";
import { cn } from "~/lib/utils";
import ImageCrop from "./file-uploader/image-crop";
import useFileUpload from "./file-uploader/use-file-upload";
import WebcamCapture from "./file-uploader/webcam-capture";
import { Loading } from "./loading";
import { Button } from "./ui/button";

type Step = "idle" | "crop" | "webcam";

interface EditableImageOverlayProps {
children: React.ReactNode;
canEdit: boolean;
folder: string;
aspectRatio?: number;
circularCrop?: boolean;
onImageSaved: (url: string) => void | Promise<void>;
isSaving?: boolean;
overlayPosition?: "top-right" | "center";
className?: string;
}

export function EditableImageOverlay({
children,
canEdit,
folder,
aspectRatio,
circularCrop,
onImageSaved,
isSaving,
overlayPosition = "top-right",
className,
}: EditableImageOverlayProps) {
const [step, setStep] = useState<Step>("idle");
const [image, setImage] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const webcamRef = useRef<Webcam>(null);
const { uploadFile } = useFileUpload();

if (!canEdit) {
return <>{children}</>;
}

const onSelectFile = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0 && e.target.files[0]) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = () => {
setImage(reader.result as string);
setStep("crop");
};
reader.onerror = () => {
toast.error("Failed to read file.");
};
reader.readAsDataURL(file);
}
};

const handleCropComplete = async (blob: Blob) => {
if (!blob) {
toast.error("No cropped image to upload.");
return;
}
const file = new File([blob], "cropped_image.jpg", { type: "image/jpeg" });
try {
setLoading(true);
const url = await uploadFile(file, folder);
setStep("idle");
setLoading(false);
await onImageSaved(url);
} catch (error) {
toast.error("Failed to upload image.");
console.error(error);
setStep("idle");
setLoading(false);
}
};

const capturePhoto = () => {
if (!webcamRef.current) return;
const imageSrc = webcamRef.current.getScreenshot();
setImage(imageSrc);
setStep("crop");
};

const isLoading = loading || isSaving;

return (
<div className={cn("group/editable relative", className)}>
{children}

{/* Hidden file input */}
<input
type="file"
accept=".png, .jpg, .jpeg, .webp"
onChange={onSelectFile}
ref={fileInputRef}
className="hidden"
/>

{/* Edit overlay */}
{overlayPosition === "top-right" ? (
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={isLoading}
aria-label="Edit image"
className="absolute top-3 right-3 z-20 gap-1.5 bg-black/50 hover:bg-black/70 text-white border-white/20 backdrop-blur-sm opacity-60 sm:opacity-0 sm:group-hover/editable:opacity-100 transition-opacity duration-200"
>
{isLoading ? (
<Loading />
) : (
<>
<CameraIcon className="size-4" />
<span className="text-xs hidden sm:inline">Edit</span>
</>
)}
</Button>
) : (
<div
onClick={() => fileInputRef.current?.click()}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
fileInputRef.current?.click();
}
}}
aria-label="Edit image"
className="absolute inset-0 z-10 flex items-center justify-center rounded-full bg-black/0 hover:bg-black/40 opacity-60 sm:opacity-0 sm:group-hover/editable:opacity-100 transition-all duration-200 cursor-pointer"
>
{isLoading ? (
<Loading />
) : (
<CameraIcon className="size-6 text-white drop-shadow-md" />
)}
</div>
)}

{/* Crop modal */}
{step === "crop" && image && (
<ImageCrop
image={image}
onComplete={handleCropComplete}
onCancel={() => setStep("idle")}
aspectRatio={aspectRatio}
circularCrop={circularCrop}
loading={loading}
/>
)}

{/* Webcam modal */}
{step === "webcam" && (
<WebcamCapture
ref={webcamRef}
capturePhoto={capturePhoto}
onCancel={() => setStep("idle")}
/>
)}
</div>
);
}
9 changes: 0 additions & 9 deletions src/components/pools/forms/update-pool-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import AreYouSureDialog from "~/components/dialogs/are-you-sure";
import { ImageUploadField } from "~/components/forms/fields/image-upload-field";
import { InputField } from "~/components/forms/fields/input-field";
import { TagsField } from "~/components/forms/fields/tags-field";
import { TextAreaField } from "~/components/forms/fields/textarea-field";
Expand Down Expand Up @@ -92,14 +91,6 @@ export function UpdatePoolForm({
placeholder=""
rows={6}
/>
<ImageUploadField
form={form}
folder="pools"
name="banner_url"
aspectRatio={16 / 9}
label="Pool Image"
placeholder="Upload banner image"
/>
<UoaField
form={form}
name="unit_of_account"
Expand Down
Loading
Loading