From 1936e9aa9bf71fc143c775d4cbea93171b1e1624 Mon Sep 17 00:00:00 2001 From: William Luke Date: Thu, 9 Apr 2026 14:42:02 +0300 Subject: [PATCH 1/2] feat: move image editing to inline overlays on hero sections Add camera icon overlays on voucher banner, voucher icon, and pool banner that trigger the upload flow directly from the hero section, matching social media UX patterns. Remove image upload fields from the settings forms since editing now happens inline. Co-Authored-By: Claude Opus 4.6 --- .../pools/[address]/pool-client-page.tsx | 36 +++- src/components/editable-image-overlay.tsx | 171 ++++++++++++++++++ .../pools/forms/update-pool-form.tsx | 9 - src/components/voucher/forms/voucher-form.tsx | 45 ++--- .../voucher/voucher-hero-section.tsx | 95 ++++++++-- src/components/voucher/voucher-page.tsx | 2 +- 6 files changed, 293 insertions(+), 65 deletions(-) create mode 100644 src/components/editable-image-overlay.tsx diff --git a/src/app/(main)/pools/[address]/pool-client-page.tsx b/src/app/(main)/pools/[address]/pool-client-page.tsx index 19175195..a970dc5e 100644 --- a/src/app/(main)/pools/[address]/pool-client-page.tsx +++ b/src/app/(main)/pools/[address]/pool-client-page.tsx @@ -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"; @@ -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 ( {/* Modern Hero Section */} -
+ {/* Banner Background */} {metadata?.banner_url ? (
@@ -142,7 +174,7 @@ export function PoolClientPage() { {/* Decorative Elements */}
-
+ {/* Modern Tabs Section */}
diff --git a/src/components/editable-image-overlay.tsx b/src/components/editable-image-overlay.tsx new file mode 100644 index 00000000..676101a4 --- /dev/null +++ b/src/components/editable-image-overlay.tsx @@ -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; + 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("idle"); + const [image, setImage] = useState(null); + const [loading, setLoading] = useState(false); + const fileInputRef = useRef(null); + const webcamRef = useRef(null); + const { uploadFile } = useFileUpload(); + + if (!canEdit) { + return <>{children}; + } + + const onSelectFile = (e: React.ChangeEvent) => { + 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 ( +
+ {children} + + {/* Hidden file input */} + + + {/* Edit overlay */} + {overlayPosition === "top-right" ? ( + + ) : ( +
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 ? ( + + ) : ( + + )} +
+ )} + + {/* Crop modal */} + {step === "crop" && image && ( + setStep("idle")} + aspectRatio={aspectRatio} + circularCrop={circularCrop} + loading={loading} + /> + )} + + {/* Webcam modal */} + {step === "webcam" && ( + setStep("idle")} + /> + )} +
+ ); +} diff --git a/src/components/pools/forms/update-pool-form.tsx b/src/components/pools/forms/update-pool-form.tsx index 8b19107f..408a0f1c 100644 --- a/src/components/pools/forms/update-pool-form.tsx +++ b/src/components/pools/forms/update-pool-form.tsx @@ -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"; @@ -92,14 +91,6 @@ export function UpdatePoolForm({ placeholder="" rows={6} /> -
- + -
- - -
- { + try { + await update.mutateAsync({ voucherAddress: address, bannerUrl: url }); + await utils.voucher.invalidate(); + toast.success("Banner updated"); + } catch (error) { + console.error(error); + toast.error("Failed to update banner"); + } + }; + + const handleIconSave = async (url: string) => { + try { + await update.mutateAsync({ voucherAddress: address, iconUrl: url }); + await utils.voucher.invalidate(); + toast.success("Icon updated"); + } catch (error) { + console.error(error); + toast.error("Failed to update icon"); + } + }; + return ( -
+ {/* Banner Background */} {voucher?.banner_url ? (
@@ -45,22 +89,33 @@ export function VoucherHeroSection({
{/* Voucher Icon and Name */}
- - - + + - - - {details?.name?.substring(0, 2).toLocaleUpperCase()} - - + > + + + + {details?.name?.substring(0, 2).toLocaleUpperCase()} + + +

{details?.name} @@ -103,6 +158,6 @@ export function VoucherHeroSection({ {/* Decorative Elements */}
-
+ ); -} \ No newline at end of file +} diff --git a/src/components/voucher/voucher-page.tsx b/src/components/voucher/voucher-page.tsx index b68fa448..3a93e47a 100644 --- a/src/components/voucher/voucher-page.tsx +++ b/src/components/voucher/voucher-page.tsx @@ -38,7 +38,7 @@ const VoucherPage = ({ title={details?.name ?? "Voucher Details"} className="bg-transparent" > - + Date: Thu, 9 Apr 2026 14:58:05 +0300 Subject: [PATCH 2/2] chore: update claude code review workflow config Co-Authored-By: Claude Opus 4.6 --- .github/workflows/claude-code-review.yml | 76 +++++++++++++++--------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 7320a21e..d46efa44 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -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 @@ -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:*)"