Fiyat
-
+
{price} ₺
diff --git a/src/components/DealDetails/DealImages.tsx b/src/components/DealDetails/DealImages.tsx
index 290247e..3419c66 100644
--- a/src/components/DealDetails/DealImages.tsx
+++ b/src/components/DealDetails/DealImages.tsx
@@ -4,14 +4,15 @@ import type { DealImage } from "../../models/deal/DealImage"
type DealImagesProps = {
images: DealImage[]
alt?: string
+ isExpired?: boolean
}
-export default function DealImages({ images, alt }: DealImagesProps) {
+export default function DealImages({ images, alt, isExpired = false }: DealImagesProps) {
const srcs = useMemo(() => {
return (images ?? [])
.filter((x) => x?.imageUrl && x.imageUrl.trim().length > 0)
.sort((a, b) => a.order - b.order)
- .slice(0, 5) // max 5
+ .slice(0, 5)
.map((x) => x.imageUrl.trim())
}, [images])
@@ -43,16 +44,21 @@ export default function DealImages({ images, alt }: DealImagesProps) {
const safeIndex = Math.min(activeIndex, srcs.length - 1)
const activeSrc = srcs[safeIndex]
+ const expiredClass = isExpired ? "grayscale opacity-90" : ""
+
return (
- {/* Main image (no crop) */}
-
+ {/* Main image */}
+
{!imgError ? (

setImgError(true)}
/>
) : (
@@ -60,24 +66,19 @@ export default function DealImages({ images, alt }: DealImagesProps) {
🖼️
-
- Görsel yüklenemedi
-
-
- Daha sonra tekrar deneyin.
-
+
Görsel yüklenemedi
+
Daha sonra tekrar deneyin.
)}
- {/* Thumbnails (crop ok for selection) */}
+ {/* Thumbnails */}
{srcs.length > 1 ? (
- {/* full-width top divider */}
-
+
{srcs.map((src, idx) => {
const active = idx === safeIndex
return (
@@ -98,8 +99,7 @@ export default function DealImages({ images, alt }: DealImagesProps) {

{}}
+ className={["w-full h-full object-cover transition", expiredClass].join(" ")}
/>
@@ -108,7 +108,6 @@ export default function DealImages({ images, alt }: DealImagesProps) {
) : null}
-
)
}
diff --git a/src/components/DealDetails/DealNotice.tsx b/src/components/DealDetails/DealNotice.tsx
new file mode 100644
index 0000000..6ccbaf7
--- /dev/null
+++ b/src/components/DealDetails/DealNotice.tsx
@@ -0,0 +1,81 @@
+// src/components/DealDetails/DealNotice.tsx
+import type { DealNoticeSeverity } from "../../models/deal/DealDetail"
+
+type DealNoticeProps = {
+ title: string
+ body?: string | null
+ severity: DealNoticeSeverity
+}
+
+const styles: Record<
+ DealNoticeSeverity,
+ { wrap: string; dotVar: string; titleVar: string; bodyVar: string }
+> = {
+ INFO: {
+ wrap:
+ "border-[color-mix(in_srgb,var(--color-notice-info)_35%,transparent)] " +
+ "bg-[linear-gradient(90deg,var(--color-notice-info-soft),var(--color-surface))] " +
+ "shadow-[0_0_0_1px_color-mix(in_srgb,var(--color-notice-info)_18%,transparent)]",
+ dotVar: "var(--color-notice-info)",
+ titleVar: "var(--color-text)",
+ bodyVar: "var(--color-text-muted)",
+ },
+ SUCCESS: {
+ wrap:
+ "border-[color-mix(in_srgb,var(--color-notice-success)_35%,transparent)] " +
+ "bg-[linear-gradient(90deg,var(--color-notice-success-soft),var(--color-surface))] " +
+ "shadow-[0_0_0_1px_color-mix(in_srgb,var(--color-notice-success)_18%,transparent)]",
+ dotVar: "var(--color-notice-success)",
+ titleVar: "var(--color-text)",
+ bodyVar: "var(--color-text-muted)",
+ },
+ WARNING: {
+ wrap:
+ "border-[color-mix(in_srgb,var(--color-notice-warning)_35%,transparent)] " +
+ "bg-[linear-gradient(90deg,var(--color-notice-warning-soft),var(--color-surface))] " +
+ "shadow-[0_0_0_1px_color-mix(in_srgb,var(--color-notice-warning)_18%,transparent)]",
+ dotVar: "var(--color-notice-warning)",
+ titleVar: "var(--color-text)",
+ bodyVar: "var(--color-text-muted)",
+ },
+ DANGER: {
+ wrap:
+ "border-[color-mix(in_srgb,var(--color-notice-danger)_35%,transparent)] " +
+ "bg-[linear-gradient(90deg,var(--color-notice-danger-soft),var(--color-surface))] " +
+ "shadow-[0_0_0_1px_color-mix(in_srgb,var(--color-notice-danger)_18%,transparent)]",
+ dotVar: "var(--color-notice-danger)",
+ titleVar: "var(--color-text)",
+ bodyVar: "var(--color-text-muted)",
+ },
+}
+
+export default function DealNotice({ title, body, severity }: DealNoticeProps) {
+ const s = styles[severity] ?? styles.INFO
+
+ return (
+
+
+
+
+
+ {title}
+
+ {body ? (
+
+ {body}
+
+ ) : null}
+
+
+
+ )
+}
diff --git a/src/components/DealDetails/SimilarDeals.tsx b/src/components/DealDetails/SimilarDeals.tsx
new file mode 100644
index 0000000..d7cff7b
--- /dev/null
+++ b/src/components/DealDetails/SimilarDeals.tsx
@@ -0,0 +1,98 @@
+import { Link } from "react-router-dom"
+import { scoreToHeat } from "../../utils/heat"
+import type { SimilarDeal } from "../../models/Shared/SimilarDeal"
+
+type SimilarDealsProps = {
+ deals: SimilarDeal[]
+}
+
+function formatTryPrice(p?: number) {
+ if (typeof p !== "number") return null
+ return `${p.toLocaleString("tr-TR")}₺`
+}
+export default function SimilarDeals({ deals }: SimilarDealsProps) {
+ if (!deals || deals.length === 0) return null
+
+ return (
+
+
+
Benzer fırsatlar
+
+ Tümünü gör
+
+
+
+
+ {deals.slice(0, 5).map((d) => {
+ const { degree, color } = scoreToHeat(d.score ?? 0)
+
+ return (
+
+ {/* Image Section */}
+
+

+
+
+ {degree}°
+
+
+
+
+ {/* Details Section */}
+
+ {/* Title - Always 2 lines max */}
+
+ {d.title}
+
+
+ {/* Seller and Price */}
+
+ {/* Seller Name */}
+
{d.sellerName ?? "Bilinmiyor"}
+ {/* Price */}
+ {typeof d.price === "number" && (
+
{formatTryPrice(d.price)}
+ )}
+
+
+
+ )
+ })}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/FilterMenu.tsx b/src/components/FilterMenu.tsx
new file mode 100644
index 0000000..8517829
--- /dev/null
+++ b/src/components/FilterMenu.tsx
@@ -0,0 +1,78 @@
+// src/components/FilterMenu.tsx
+import React, { useState } from "react";
+
+type FilterMenuProps = {
+ onFilterChange: (filters: { [key: string]: boolean }) => void;
+};
+
+const FilterMenu: React.FC
= ({ onFilterChange }) => {
+ const [filtersVisible, setFiltersVisible] = useState(false);
+ const [selectedFilters, setSelectedFilters] = useState<{ [key: string]: boolean }>({
+ filter1: false,
+ filter2: false,
+ filter3: false,
+ });
+
+ const toggleFilterMenu = () => {
+ setFiltersVisible(!filtersVisible);
+ };
+
+ const handleCheckboxChange = (e: React.ChangeEvent) => {
+ const { name, checked } = e.target;
+ const updatedFilters = { ...selectedFilters, [name]: checked };
+ setSelectedFilters(updatedFilters);
+ onFilterChange(updatedFilters); // Pass the updated filters to parent
+ };
+
+ return (
+
+ {/* Filtreler Butonu */}
+
+
+ {/* Filtre Menüsü */}
+ {filtersVisible && (
+
+ )}
+
+ );
+};
+
+export default FilterMenu;
diff --git a/src/components/Layout/AppShell.tsx b/src/components/Layout/AppShell.tsx
new file mode 100644
index 0000000..8178afc
--- /dev/null
+++ b/src/components/Layout/AppShell.tsx
@@ -0,0 +1,51 @@
+import React, { useEffect, useMemo, useState } from "react"
+import { useLocation, useNavigate } from "react-router-dom"
+import Navbar from "./Navbar/Navbar"
+import CategoriesSidebar from "../Categories/CategoriesSidebar"
+import { DUMMY_CATEGORIES } from "../../data/categories"
+
+export default function AppShell({ children }: { children: React.ReactNode }) {
+ const [catsOpen, setCatsOpen] = useState(false)
+ const location = useLocation()
+ const navigate = useNavigate()
+
+ const activeSlug = useMemo(() => {
+ const params = new URLSearchParams(location.search)
+ return params.get("cat")
+ }, [location.search])
+
+ // route değişince sidebar kapansın
+ useEffect(() => {
+ setCatsOpen(false)
+ }, [location.pathname, location.search])
+
+ const setCategory = (slug: string) => {
+ const params = new URLSearchParams(location.search)
+ params.set("cat", slug)
+ navigate({ pathname: location.pathname, search: params.toString() }, { replace: false })
+ }
+console.log("APPSHELL render, catsOpen:", catsOpen)
+
+ return (
+
+ {
+ console.log("toggle in shell")
+ setCatsOpen((v) => !v)
+ }}
+/>
+
+
+ setCatsOpen(false)}
+ onSelect={setCategory}
+ />
+
+
+ {children}
+
+ )
+}
diff --git a/src/components/Layout/Footer.tsx b/src/components/Layout/Footer.tsx
index 5337696..46b3889 100644
--- a/src/components/Layout/Footer.tsx
+++ b/src/components/Layout/Footer.tsx
@@ -1,8 +1,43 @@
export default function Footer() {
return (
-