Update carousels, card hover, and IranBay branding.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-04 13:21:27 +03:30
co-authored by Cursor
commit 1153e8e78f
178 changed files with 30517 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import Cropper, { type Area } from "react-easy-crop";
import { getCroppedImageFile } from "@/lib/cropImage";
import styles from "./ImageCropDialog.module.css";
const MIN_ZOOM = 0.2;
const MAX_ZOOM = 3;
const ZOOM_STEP = 0.05;
type Props = {
open: boolean;
imageSrc: string | null;
title: string;
aspect: number;
/** Prefer PNG for logos with transparency */
outputType?: "image/jpeg" | "image/png";
fileName?: string;
onCancel: () => void;
onComplete: (file: File, previewUrl: string) => void;
};
export default function ImageCropDialog({
open,
imageSrc,
title,
aspect,
outputType = "image/jpeg",
fileName = "image",
onCancel,
onComplete,
}: Props) {
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const transparentOutput = outputType === "image/png";
useEffect(() => {
if (!open) return;
setCrop({ x: 0, y: 0 });
setZoom(1);
setCroppedAreaPixels(null);
setBusy(false);
setError("");
}, [open, imageSrc]);
const onCropComplete = useCallback((_: Area, pixels: Area) => {
setCroppedAreaPixels(pixels);
}, []);
function clampZoom(value: number) {
return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Number(value.toFixed(2))));
}
if (!open || !imageSrc) return null;
async function apply() {
if (!croppedAreaPixels || !imageSrc) return;
setBusy(true);
setError("");
try {
const file = await getCroppedImageFile(
imageSrc,
croppedAreaPixels,
fileName,
outputType,
);
const previewUrl = URL.createObjectURL(file);
onComplete(file, previewUrl);
setCrop({ x: 0, y: 0 });
setZoom(1);
} catch (err) {
setError(err instanceof Error ? err.message : "Could not crop image.");
} finally {
setBusy(false);
}
}
return (
<div className={styles.overlay} role="dialog" aria-modal aria-label={title}>
<div className={styles.dialog}>
<div className={styles.head}>
<h2 className={styles.title}>{title}</h2>
<p className={styles.hint}>
Drag to move · zoom out to add padding around the image
</p>
</div>
<div
className={
transparentOutput
? `${styles.stage} ${styles.stageLight}`
: styles.stage
}
>
<Cropper
image={imageSrc}
crop={crop}
zoom={zoom}
minZoom={MIN_ZOOM}
maxZoom={MAX_ZOOM}
aspect={aspect}
onCropChange={setCrop}
onZoomChange={(value) => setZoom(clampZoom(value))}
onCropComplete={onCropComplete}
showGrid={false}
objectFit="contain"
restrictPosition={false}
style={{
containerStyle: {
background: "transparent",
},
}}
/>
</div>
<div className={styles.zoomRow}>
<button
type="button"
className={styles.zoomBtn}
disabled={busy || zoom <= MIN_ZOOM}
aria-label="Zoom out"
onClick={() => setZoom((value) => clampZoom(value - ZOOM_STEP))}
>
−
</button>
<input
className={styles.zoomRange}
type="range"
min={MIN_ZOOM}
max={MAX_ZOOM}
step={0.01}
value={zoom}
disabled={busy}
aria-label="Zoom"
onChange={(event) => setZoom(clampZoom(Number(event.target.value)))}
/>
<button
type="button"
className={styles.zoomBtn}
disabled={busy || zoom >= MAX_ZOOM}
aria-label="Zoom in"
onClick={() => setZoom((value) => clampZoom(value + ZOOM_STEP))}
>
+
</button>
<span className={styles.zoomValue}>{Math.round(zoom * 100)}%</span>
</div>
{error ? (
<p className={styles.error} role="alert">
{error}
</p>
) : null}
<div className={styles.actions}>
<button
type="button"
className={`outline-btn ${styles.cancelBtn}`}
disabled={busy}
onClick={() => {
setCrop({ x: 0, y: 0 });
setZoom(1);
setError("");
onCancel();
}}
>
Cancel
</button>
<button
type="button"
className={`outline-btn ${styles.applyBtn}`}
disabled={busy || !croppedAreaPixels}
onClick={() => void apply()}
>
{busy ? "Applying…" : "Apply crop"}
</button>
</div>
</div>
</div>
);
}