fix: styling in hero and add imageblurry

This commit is contained in:
Sabda Yagra 2025-08-14 21:19:12 +07:00
parent 7e1ffe2d2f
commit fb976f01bb
7 changed files with 252 additions and 62 deletions

View File

@ -64,6 +64,17 @@ type Category = {
name: string;
};
type PlacementType = "all" | "mabes" | "polda" | "international" | string;
interface FilePlacement {
mediaFileId: number;
placements?: PlacementType[];
}
interface TempFileItem {
id: number | string;
}
type Detail = {
id: string;
title: string;
@ -255,6 +266,7 @@ export default function FormImageUpdate() {
setDetail(details);
setSelectedTarget(String(details.category.id));
setTempFile(details?.files);
setValue("title", details.title);
setValue("description", details.htmlDescription);
@ -641,6 +653,77 @@ export default function FormImageUpdate() {
</Button>
</div>
));
type PlacementType = "all" | "mabes" | "polda" | "international" | string;
interface FilePlacement {
mediaFileId: number;
placements?: PlacementType[];
}
interface TempFileItem {
id: number | string;
// tambahkan properti lain kalau ada
}
const [tempFile, setTempFile] = useState<TempFileItem[]>([]);
const [filePlacements, setFilePlacements] = useState<FilePlacement[]>([]);
const setupPlacement = (id: number | string, placement: PlacementType) => {
console.log(`FileDestination.leng:: ${id}_${placement}`);
const arrayFile: FilePlacement[] = [];
for (let i = 0; i < tempFile?.length; i++) {
const element = tempFile[i];
if (element.id == id) {
const findPlacementIdx = filePlacements.findIndex(
(o) => Number(o.mediaFileId) === Number(id)
);
if (findPlacementIdx > -1) {
const findPlacement = filePlacements[findPlacementIdx];
if (findPlacement?.placements?.includes(placement)) {
if (placement === "all") {
findPlacement.placements = undefined;
} else {
findPlacement.placements = findPlacement.placements.filter(
(val) => val !== placement
);
if (findPlacement.placements?.includes("all")) {
findPlacement.placements = findPlacement.placements.filter(
(val) => val !== "all"
);
}
}
} else if (placement === "all") {
findPlacement.placements = [
"all",
"mabes",
"polda",
"international",
];
} else if (findPlacement.placements) {
findPlacement.placements = [...findPlacement.placements, placement];
} else {
findPlacement.placements = [placement];
}
} else {
const file: FilePlacement = {
mediaFileId: Number(element.id),
placements: [placement],
};
arrayFile.push(file);
}
}
}
const finalPlacements = [...filePlacements, ...arrayFile];
setFilePlacements(finalPlacements);
console.log("FileDestination.leng::", finalPlacements);
};
const handleCheckboxChangeImage = (fileId: number, value: string) => {
setSelectedOptions((prev: any) => {
@ -894,11 +977,8 @@ export default function FormImageUpdate() {
checked={selectedOptions[
file.id
]?.includes("all")}
onChange={() =>
handleCheckboxChangeImage(
file.id,
"all"
)
onChange={(e) =>
setupPlacement(file.id, e.target.value)
}
className="form-checkbox"
/>

View File

@ -1,13 +1,23 @@
import { getCategoryData, getPublicCategoryData } from "@/service/landing/landing";
import {
getCategoryData,
getPublicCategoryData,
} from "@/service/landing/landing";
import React, { useEffect, useState } from "react";
import { Reveal } from "./Reveal";
import { useTranslations } from "next-intl";
import { usePathname } from "next/navigation";
import { useParams } from "next/navigation";
import Image from "next/image";
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "../ui/carousel";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "../ui/carousel";
import { useRouter } from "@/i18n/routing";
import { Button } from "../ui/button";
import ImageBlurry from "../ui/image-blurry";
const ContentCategory = (props: { group?: string; type: string }) => {
const [categories, setCategories] = useState<any>();
@ -19,14 +29,26 @@ const ContentCategory = (props: { group?: string; type: string }) => {
const satkerName = params?.satker_name;
const router = useRouter();
let prefixPath = poldaName ? `/polda/${poldaName}` : satkerName ? `/satker/${satkerName}` : "/";
let prefixPath = poldaName
? `/polda/${poldaName}`
: satkerName
? `/satker/${satkerName}`
: "/";
useEffect(() => {
initFetch();
}, []);
const initFetch = async () => {
const response = await getPublicCategoryData(
props.group == "mabes" ? "" : props.group == "polda" && poldaName && String(poldaName)?.length > 1 ? poldaName : props.group == "satker" && satkerName && String(satkerName)?.length > 1 ? "satker-" + satkerName : "",
props.group == "mabes"
? ""
: props.group == "polda" && poldaName && String(poldaName)?.length > 1
? poldaName
: props.group == "satker" &&
satkerName &&
String(satkerName)?.length > 1
? "satker-" + satkerName
: "",
"",
locale == "en" ? true : false
);
@ -52,7 +74,10 @@ const ContentCategory = (props: { group?: string; type: string }) => {
<animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
</svg>`;
const toBase64 = (str: string) => (typeof window === "undefined" ? Buffer.from(str).toString("base64") : window.btoa(str));
const toBase64 = (str: string) =>
typeof window === "undefined"
? Buffer.from(str).toString("base64")
: window.btoa(str);
return (
<div className="px-4 lg:px-0 py-10">
@ -61,22 +86,34 @@ const ContentCategory = (props: { group?: string; type: string }) => {
<h2 className="text-start text-lg md:text-xl font-bold text-[#bb3523] border-b-2 border-[#bb3523] mb-4 uppercase">
{pathname?.split("/")[1] == "in" ? (
<>
<span className="text-[#bb3523] dark:text-white">{t("category", { defaultValue: "Category" })}&nbsp;</span>
<span className="text-[#bb3523] dark:text-white">
{t("category", { defaultValue: "Category" })}&nbsp;
</span>
{t("content", { defaultValue: "Content" })}
</>
) : (
<>
<span className="text-[#bb3523] dark:text-white">{t("content", { defaultValue: "Content" })}&nbsp;</span>
<span className="text-[#bb3523] dark:text-white">
{t("content", { defaultValue: "Content" })}&nbsp;
</span>
{t("category", { defaultValue: "Category" })}
</>
)}
</h2>
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4">
{(seeAllValue ? categories : categories?.slice(0, 4))?.map((category: any) => (
{(seeAllValue ? categories : categories?.slice(0, 4))?.map(
(category: any) => (
<div key={category?.id}>
<div onClick={() => router.push(`${prefixPath}all/filter?category=${category?.id}`)} className="cursor-pointer relative group rounded-md overflow-hidden shadow-md hover:shadow-lg block">
<div
onClick={() =>
router.push(
`${prefixPath}all/filter?category=${category?.id}`
)
}
className="cursor-pointer relative group rounded-md overflow-hidden shadow-md hover:shadow-lg block"
>
{/* Gambar */}
<Image
{/* <Image
priority={true}
placeholder={`data:image/svg+xml;base64,${toBase64(shimmer(700, 475))}`}
alt="category"
@ -84,6 +121,15 @@ const ContentCategory = (props: { group?: string; type: string }) => {
height={1440}
src={category?.smallThumbnailLink}
className="w-full lg:h-[300px] h-40 object-cover group-hover:scale-110 transition-transform duration-300"
/> */}
<ImageBlurry
src={category?.smallThumbnailLink}
alt="gambar-utama"
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
)}`}
priority={true}
className="w-full lg:h-[300px] h-40 object-contain group-hover:scale-110 transition-transform duration-300"
/>
{/* Overlay gelap */}
@ -91,18 +137,26 @@ const ContentCategory = (props: { group?: string; type: string }) => {
{/* Judul */}
<div className="absolute bottom-5 left-1/2 transform -translate-x-1/2 text-white">
<h3 className="text-sm font-semibold text-center">{category?.name}</h3>
<h3 className="text-sm font-semibold text-center">
{category?.name}
</h3>
</div>
</div>
</div>
))}
)
)}
</div>
{/* Tombol See More / See Less */}
{categories?.length > 4 && (
<div className="flex items-center flex-row justify-center mt-6">
<Button onClick={() => setSeeAllValue(!seeAllValue)} className="bg-white dark:bg-black hover:bg-[#bb3523] text-[#bb3523] hover:text-white border-2 border-[#bb3523]">
{seeAllValue ? t("seeLess", { defaultValue: "See Less" }) : t("seeMore", { defaultValue: "See More" })}
<Button
onClick={() => setSeeAllValue(!seeAllValue)}
className="bg-white dark:bg-black hover:bg-[#bb3523] text-[#bb3523] hover:text-white border-2 border-[#bb3523]"
>
{seeAllValue
? t("seeLess", { defaultValue: "See Less" })
: t("seeMore", { defaultValue: "See More" })}
</Button>
</div>
)}

View File

@ -50,6 +50,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
import { Link } from "@/i18n/routing";
import { listBannerHero } from "@/service/settings/settings";
import ImageBlurry from "../ui/image-blurry";
import { Icon } from "@iconify/react/dist/iconify.js";
type HeroModalProps = {
onClose: () => void;
@ -173,7 +174,7 @@ const HeroModal = ({
>
</button>
{/* <Image
<Image
priority={true}
src={list?.smallThumbnailLink}
alt="gambar-utama"
@ -182,17 +183,14 @@ const HeroModal = ({
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
)}`}
className="w-full h-[310px] lg:h-[420px] rounded-lg object-cover"
/> */}
<ImageBlurry
className="w-full h-[310px] lg:h-[420px] rounded-lg object-contain bg-black"
/>
{/* <ImageBlurry
priority={true}
src={list?.smallThumbnailLink}
alt="gambar-utama"
style={{
objectFit: "contain",
width: "100%",
height: "100%",
}}
/>
className="w-full h-[310px] lg:h-[420px] rounded-lg object-contain"
/> */}
<div className="absolute bottom-0 left-0 right-0 bg-black/30 backdrop-brightness-50 text-white pb-4 px-4 pt-8 rounded-bl-2xl rounded-tr-2xl mx-3 mb-2">
<div className="absolute top-0 left-0 bottom-0 w-2 bg-[#bb3523] rounded-bl-lg"></div>
<span className="absolute top-0 left-0 mt-2 mb-3 mx-3 bg-[#bb3523] text-white text-xs font-semibold uppercase px-2 py-1 rounded">
@ -577,24 +575,29 @@ const HeroNew = (props: { group?: string }) => {
? `${prefixPath}/document/detail/${list?.slug}`
: `${prefixPath}/audio/detail/${list?.slug}`
}
className="absolute bottom-20 left-8 lg:left-32 z-20 text-white w-[85%] lg:w-[45%] cursor-pointer"
className="absolute bottom-10 lg:bottom-20 left-8 lg:left-32 z-20 text-white w-[85%] lg:w-[45%] cursor-pointer"
>
<span className="text-red-600 text-lg font-bold uppercase">
<span className="text-red-600 text-[10px] lg:text-lg font-bold uppercase">
{list?.categoryName}
</span>
<h2 className="text-xl font-bold">{list?.title}</h2>
<p className="text-sm mt-2">
<h2 className="text-[14px] lg:text-xl font-bold">{list?.title}</h2>
<p className="text-[9px] lg:text-sm mt-2">
{formatDateToIndonesian(new Date(list?.createdAt))}{" "}
{list?.timezone || "WIB"} | 👁 {list?.clickCount}
</p>
</Link>
{/* Tombol navigasi */}
<div className="swiper-button-prev absolute left-6 top-[45%] z-30 -translate-y-1/2 hover:bg-black/70 text-white p-2 rounded-full">
<ChevronLeft className="w-5 h-5" />
<div className="absolute left-2 top-[45%] z-30 -translate-y-1/2 hover:bg-black/70 text-white p-2 rounded-full">
<Icon
icon="mdi:chevron-left"
className="w-5 lg:w-10 h-5 lg:h-10"
/>{" "}
</div>
<div className="swiper-button-next absolute right-6 top-[45%] z-30 -translate-y-1/2 hover:bg-black/70 text-white p-2 rounded-full">
<ChevronRight className="w-5 h-5" />
<div className="absolute right-2 top-[45%] z-30 -translate-y-1/2 hover:bg-black/70 text-white p-2 rounded-full">
<Icon
icon="mdi:chevron-right"
className="w-5 lg:w-10 h-5 lg:h-10"
/>{" "}
</div>
</div>
</SwiperSlide>

View File

@ -18,6 +18,7 @@ import { useTranslations } from "next-intl";
import { Skeleton } from "../ui/skeleton";
import Image from "next/image";
import { motion } from "framer-motion";
import ImageBlurry from "../ui/image-blurry";
const NewContent = (props: { group: string; type: string }) => {
const [newContent, setNewContent] = useState<any>();
@ -228,7 +229,7 @@ const NewContent = (props: { group: string; type: string }) => {
}
>
{" "}
<Image
{/* <Image
priority={true}
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
@ -238,6 +239,15 @@ const NewContent = (props: { group: string; type: string }) => {
alt="image"
src={image?.smallThumbnailLink}
className="w-full h-full object-cover"
/> */}
<ImageBlurry
src={image?.smallThumbnailLink}
alt="image"
priority={true}
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
)}`}
className="w-full h-full object-contain"
/>
</Link>
</motion.div>
@ -431,7 +441,7 @@ const NewContent = (props: { group: string; type: string }) => {
prefixPath + `/video/detail/${video?.slug}`
}
>
<Image
{/* <Image
priority={true}
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
@ -441,6 +451,15 @@ const NewContent = (props: { group: string; type: string }) => {
height={1440}
src={video?.smallThumbnailLink}
className="w-full h-full object-cover"
/> */}
<ImageBlurry
src={video?.smallThumbnailLink}
alt="video"
priority={true}
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
)}`}
className="w-full h-full object-contain"
/>
</Link>
</motion.div>

View File

@ -15,6 +15,7 @@ import { getHeroData } from "@/service/landing/landing";
import { htmlToString } from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import { Button } from "../ui/button";
import ImageBlurry from "../ui/image-blurry";
const ScrollableContent = () => {
const [contentType, setContentType] = useState("all");
@ -161,7 +162,10 @@ const ScrollableContent = () => {
</div>
<button
onClick={() =>
router.push(prefixPath + `/${contentType}/filter?title=${search.toLowerCase()}`)
router.push(
prefixPath +
`/${contentType}/filter?title=${search.toLowerCase()}`
)
}
className="flex justify-center items-center px-6 w-full lg:w-[20%] py-4 bg-[#bb3523] gap-2 text-white rounded-lg hover:bg-red-700 text-[14px]"
>
@ -206,12 +210,22 @@ const ScrollableContent = () => {
: `${prefixPath}/audio/detail/${item?.slug}`
}
>
<Image
{/* <Image
priority={true}
src={item?.smallThumbnailLink}
alt={item?.title}
fill
objectFit="cover"
/> */}
<ImageBlurry
priority={true}
src={item?.smallThumbnailLink}
alt="gambar-utama"
style={{
objectFit: "contain",
width: "100%",
height: "100%",
}}
/>
<div className="absolute top-2 right-2 bg-[#c03724] rounded-full p-1 shadow">
<svg
@ -325,12 +339,22 @@ const ScrollableContent = () => {
: `${prefixPath}/audio/detail/${item?.slug}`
}
>
<Image
{/* <Image
priority={true}
src={item?.smallThumbnailLink}
alt={item?.title}
fill
objectFit="cover"
/> */}
<ImageBlurry
priority={true}
src={item?.smallThumbnailLink}
alt="gambar-utama"
style={{
objectFit: "contain",
width: "100%",
height: "100%",
}}
/>
<div className="absolute top-2 right-2 bg-[#c03724] rounded-full p-1 shadow">
<svg

View File

@ -3,15 +3,22 @@ import React, { useEffect, useState } from "react";
interface ImageBlurryProps {
src: string;
alt?: string;
classname?: string;
className?: string;
key?: string | number;
style?: React.CSSProperties;
priority?: boolean;
placeholder?: string;
}
const ImageBlurry: React.FC<ImageBlurryProps> = (props) => {
const { src, alt, classname, key, style, priority = false } = props;
const ImageBlurry: React.FC<ImageBlurryProps> = ({
src,
alt,
className,
key,
style,
priority = false,
placeholder,
}) => {
const [imgSrc, setImgSrc] = useState<string>(src);
const [isError, setIsError] = useState<boolean>(false);
@ -26,7 +33,7 @@ const ImageBlurry: React.FC<ImageBlurryProps> = (props) => {
checkImage(pic);
} else {
pic.addEventListener("load", function () {
checkImage(this);
checkImage(this as HTMLImageElement);
});
}
});
@ -48,12 +55,10 @@ const ImageBlurry: React.FC<ImageBlurryProps> = (props) => {
};
return (
// <div className="relative overflow-hidden bg-[#e9e9e9] blurry-wrapper">
// <div className="absolute -top-6 -bottom-6 left-0 -right-6 bg-[#e9e9e9] blur-[8px] scale-[1.8] bg-center bg-no-repeat bg-contain blur-bg" style={{ backgroundImage: `url(${imgSrc})` }}></div>
// <div className="absolute inset-0 bg-transparent bg-center bg-no-repeat bg-contain image-bg" style={{ backgroundImage: `url(${imgSrc})` }}></div>
// </div>
<div
className="blurry-wrapper relative overflow-hidden bg-[#e9e9e9] rounded-t-lg"
className={`blurry-wrapper relative overflow-hidden bg-[#e9e9e9] rounded-t-lg ${
className || ""
}`}
key={key}
style={style}
>

View File

@ -176,6 +176,11 @@ export async function createMedia(data: any) {
return httpPostInterceptor(url, data);
}
export async function updateFilePlacements(data: any) {
const url = "media/file/update-placement";
return httpPostInterceptor(url, data);
}
export async function uploadThumbnail(id: any, data: any) {
const url = `media/upload?id=${id}&operation=thumbnail`;
const headers = {