feat:pwa, fix:ads video

This commit is contained in:
Rama Priyanto 2025-04-16 15:51:12 +07:00
parent 4be33b9111
commit 203b4bdea5
16 changed files with 5324 additions and 102 deletions

View File

@ -66,9 +66,13 @@ export default function AdvertisePage() {
setFiles(acceptedFiles.map((file) => Object.assign(file))); setFiles(acceptedFiles.map((file) => Object.assign(file)));
}, },
maxFiles: 1, maxFiles: 1,
accept: { accept:
"image/*": [], placement === "banner"
}, ? {
"image/*": [],
"video/*": [],
}
: { "image/*": [] },
}); });
type UserSettingSchema = z.infer<typeof createArticleSchema>; type UserSettingSchema = z.infer<typeof createArticleSchema>;
const { const {

View File

@ -44,6 +44,9 @@ export default function RootLayout({ children }: { children: ReactNode }) {
media="(prefers-color-scheme: dark)" media="(prefers-color-scheme: dark)"
/> />
<meta property="og:image" content="/logohumas.png" /> <meta property="og:image" content="/logohumas.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="divhumas.png" />
<meta name="theme-color" content="#000000" />
<LoadScript /> <LoadScript />
</head> </head>

View File

@ -2712,3 +2712,23 @@ export const ExportIcon = ({
<path fill="none" d="M0 0h36v36H0z" /> <path fill="none" d="M0 0h36v36H0z" />
</svg> </svg>
); );
export const VideoIcon = ({
size,
height = 37,
width = 32,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 37 32"
>
<g fill="currentColor">
<path d="M7.5 0h-6C.631 0 0 .631 0 1.5v29c0 .869.631 1.5 1.5 1.5h34c.869 0 1.5-.631 1.5-1.5v-29c0-.869-.631-1.5-1.5-1.5zM1 30.5v-29c0-.313.187-.5.5-.5H7v30H1.5c-.313 0-.5-.187-.5-.5m7 .5V1h21v30zM36 1.5v29c0 .313-.187.5-.5.5H30V1h5.5c.313 0 .5.187.5.5" />
<path d="M14.777 10.084a.5.5 0 0 0-.514-.025a.5.5 0 0 0-.263.441v12a.5.5 0 0 0 .777.416l9-6a.5.5 0 0 0 0-.832zM15 21.566V11.434l7.599 5.066zM5 8H3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1M3 5h2a.5.5 0 0 0 0-1H3a.5.5 0 0 0 0 1m2 7H3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1m0 4H3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1m0 4H3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1m0 4H3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1m0 4H3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1M32 9h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1m0-4h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1m0 8h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1m0 4h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1m0 4h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1m0 4h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1m2 3h-2a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1" />
</g>
</svg>
);

View File

@ -8,53 +8,61 @@ import {
CircularProgress, CircularProgress,
ScrollShadow, ScrollShadow,
} from "@heroui/react"; } from "@heroui/react";
import { ChevronLeftIcon, ChevronRightIcon, EyeIcon } from "../icons"; import {
ChevronLeftIcon,
ChevronRightIcon,
EyeIcon,
VideoIcon,
} from "../icons";
import { Swiper, SwiperSlide, useSwiper } from "swiper/react"; import { Swiper, SwiperSlide, useSwiper } from "swiper/react";
import "swiper/css"; import "swiper/css";
import "swiper/css/navigation"; import "swiper/css/navigation";
import Link from "next/link"; import Link from "next/link";
import { Autoplay, Pagination, Navigation, Controller } from "swiper/modules"; import { Autoplay, Pagination, Navigation, Controller } from "swiper/modules";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { getAdvertise } from "@/service/advertisement"; import { getAdvertise } from "@/service/advertisement";
import VideoPlayer from "../player/video-player";
const datas = [
{ id: 1, src: "/sample-banner.png" },
{ id: 2, src: "/sample-banner-2.png" },
{ id: 2, src: "/sample-banner-3.jpg" },
];
interface Jumbotron { interface Jumbotron {
id: number; id: number;
title: string; title: string;
description: string; description: string;
redirectLink: string; redirectLink: string;
contentFileUrl: string;
} }
export default function AddsCarousel() { export default function AddsCarousel() {
const [jumbotronList, setJumbotronList] = useState<Jumbotron[]>([]); const [jumbotronList, setJumbotronList] = useState<Jumbotron[]>([]);
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
const [thumbnail, setThumbnail] = useState<string | null>(null);
const [swiperInstance, setSwiperInstance] = useState<any>(null);
const [selectedVideo, setSelectedVideo] = useState(0);
useEffect(() => { useEffect(() => {
initFetch(); initFetch();
}, []); }, []);
const initFetch = async () => { const initFetch = async () => {
const req = { page: 1, limit: 5, placement: "banner" }; const req = { page: 1, limit: 5, placement: "banner", isPublish: true };
const res = await getAdvertise(req); const res = await getAdvertise(req);
setJumbotronList(res?.data?.data); setJumbotronList(res?.data?.data);
}; };
return ( return (
<div className="w-[90%] lg:w-[75%] mx-auto"> <div className="w-[90%] lg:w-[75%] mx-auto">
{jumbotronList ? ( {jumbotronList ? (
<Swiper <Swiper
centeredSlides={true} centeredSlides={true}
autoplay={{ autoplay={{
delay: 5000, delay: 50000,
disableOnInteraction: false, disableOnInteraction: true,
}} }}
navigation={true} navigation={true}
modules={[Autoplay, Controller, Navigation]} modules={[Autoplay, Controller, Navigation]}
className="mySwiper" className="mySwiper"
onSwiper={(swiper) => { onSwiper={(swiper) => {
setSwiperInstance(swiper);
swiper.navigation.nextEl?.classList.add( swiper.navigation.nextEl?.classList.add(
"bg-white/70", "bg-white/70",
"!text-black", "!text-black",
@ -70,6 +78,14 @@ export default function AddsCarousel() {
"!h-[40px]" "!h-[40px]"
); );
}} }}
onSlideChange={() => {
videoRefs.current.forEach((video) => {
if (video && !video.paused) {
video.pause();
setSelectedVideo(0);
}
});
}}
> >
{jumbotronList?.map((newsItem, index) => ( {jumbotronList?.map((newsItem, index) => (
<SwiperSlide key={newsItem?.id} className="h-[20vh] lg:h-[50vh]"> <SwiperSlide key={newsItem?.id} className="h-[20vh] lg:h-[50vh]">
@ -78,14 +94,59 @@ export default function AddsCarousel() {
radius="lg" radius="lg"
className="border-none h-[20vh] lg:h-[50vh] shadow-none" className="border-none h-[20vh] lg:h-[50vh] shadow-none"
> >
<Image {newsItem.contentFileUrl.includes(".mp4") ? (
alt="headernews" selectedVideo === newsItem?.id ? (
width={1440} <VideoPlayer
height={1080} ref={(el) => (videoRefs.current[index] = el)}
className="w-full h-[20vh] lg:!h-[50vh] object-cover rounded-lg" url={newsItem.contentFileUrl}
// src={newsItem?.src == "" ? "/no-image.jpg" : newsItem?.src} onPlay={() => {
src={datas[index % 3].src} if (swiperInstance) swiperInstance.autoplay.stop();
/> }}
onPause={() => {
if (swiperInstance) swiperInstance.autoplay.start();
}}
/>
) : (
<div className="bg-[#f0f0f0] dark:bg-stone-800 w-full h-full flex justify-center items-center">
<a
className="cursor-pointer"
onClick={() => setSelectedVideo(newsItem?.id)}
>
<VideoIcon size={90} />
</a>
</div>
)
) : (
// thumbnail ? (
// selectedVideo === newsItem?.id ? (
// <VideoPlayer url={newsItem.contentFileUrl} />
// ) : (
// <Image
// src={thumbnail}
// alt="Video thumbnail"
// width={1440}
// height={1080}
// />
// )
// ) : (
// <video
// ref={videoRef}
// src="https://kontenhumas.com/api/advertisement/viewer/inisiatif-jumat-berkah-oleh-polres-simalungun-kapolsek-tanah-jawa-bagikan-sembako-untuk-warga-kurang-mampu-49_265348.mp4"
// />
// )
<Image
alt="headernews"
width={1440}
height={1080}
className="w-full h-[20vh] lg:!h-[50vh] object-cover rounded-lg"
src={
newsItem?.contentFileUrl == ""
? "/no-image.jpg"
: newsItem?.contentFileUrl
}
// src={datas[index % 3].src}
/>
)}
</Card> </Card>
</SwiperSlide> </SwiperSlide>
))} ))}

View File

@ -0,0 +1,40 @@
// components/VideoPlayer.js
import React, { forwardRef } from "react";
type VideoPlayerProps = {
url: string;
onPlay?: () => void;
onPause?: () => void;
};
const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(
({ url, onPause, onPlay }, ref) => {
return (
<div className="video-player object-cover">
<video
ref={ref}
controls
preload="metadata"
onPlay={onPlay}
onPause={onPause}
>
<source src={url} type="video/mp4" />
Your browser does not support the video tag.
</video>
<style jsx>{`
.video-player {
max-width: 100%;
margin: 0 auto;
}
video {
width: 100%;
height: auto;
}
`}</style>
</div>
);
}
);
VideoPlayer.displayName = "VideoPlayer";
export default VideoPlayer;

View File

@ -18,6 +18,7 @@ import {
Chip, Chip,
ChipProps, ChipProps,
Checkbox, Checkbox,
Switch,
} from "@heroui/react"; } from "@heroui/react";
import { Button } from "@heroui/button"; import { Button } from "@heroui/button";
import React, { Key, useCallback, useEffect, useMemo, useState } from "react"; import React, { Key, useCallback, useEffect, useMemo, useState } from "react";
@ -31,8 +32,11 @@ import {
SearchIcon, SearchIcon,
} from "@/components/icons"; } from "@/components/icons";
import Link from "next/link"; import Link from "next/link";
import { getAllUserLevels } from "@/services/user-levels/user-levels-service"; import {
import { close, loading } from "@/config/swal"; changeIsApproval,
getAllUserLevels,
} from "@/services/user-levels/user-levels-service";
import { close, error, loading } from "@/config/swal";
import { stringify } from "querystring"; import { stringify } from "querystring";
type UserObject = { type UserObject = {
@ -43,6 +47,7 @@ type UserObject = {
parentLevelId: string; parentLevelId: string;
provinceId: string; provinceId: string;
status: string; status: string;
isApprovalActive: boolean;
}; };
export default function MasterUserLevelTable() { export default function MasterUserLevelTable() {
@ -53,6 +58,7 @@ export default function MasterUserLevelTable() {
const [userLevelAll, setUserLevelAll] = useState<UserObject[]>([]); const [userLevelAll, setUserLevelAll] = useState<UserObject[]>([]);
const [selectAllLevel, setSelectAllLevel] = useState(false); const [selectAllLevel, setSelectAllLevel] = useState(false);
const [selectedLevel, setSelectedLevel] = useState<string[]>([]); const [selectedLevel, setSelectedLevel] = useState<string[]>([]);
const [isSelected, setIsSelected] = useState(true);
const columns = [ const columns = [
{ name: "No", uid: "no" }, { name: "No", uid: "no" },
@ -69,11 +75,16 @@ export default function MasterUserLevelTable() {
{ name: "User Name", uid: "aliasName" }, { name: "User Name", uid: "aliasName" },
{ name: "Level Number", uid: "levelNumber" }, { name: "Level Number", uid: "levelNumber" },
{ name: "Parent", uid: "parentLevelId" }, { name: "Parent", uid: "parentLevelId" },
{ name: "Need Approval", uid: "approvalActive" },
{ name: "Action", uid: "actions" }, { name: "Action", uid: "actions" },
]; ];
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
useEffect(() => {
console.log("level", selectedLevel);
}, [selectedLevel]);
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, [page, selectedLevel]); }, [page, selectedLevel]);
@ -91,7 +102,7 @@ export default function MasterUserLevelTable() {
}; };
const res = await getAllUserLevels(request); const res = await getAllUserLevels(request);
const data = res?.data?.data; const data = res?.data?.data;
setTotalPage(Math.ceil(res?.data?.meta?.totalPage / 10)); setTotalPage(Math.ceil(res?.data?.meta?.totalPage));
await initUserData(10, data); await initUserData(10, data);
close(); close();
} }
@ -111,7 +122,7 @@ export default function MasterUserLevelTable() {
temp.push(String(element.id)); temp.push(String(element.id));
} }
} }
setSelectedLevel(temp); // setSelectedLevel(temp);
close(); close();
} }
@ -195,7 +206,11 @@ export default function MasterUserLevelTable() {
const cellValue = masterUserLevel[columnKey as keyof UserObject]; const cellValue = masterUserLevel[columnKey as keyof UserObject];
switch (columnKey) { switch (columnKey) {
case "parentLevelId": case "parentLevelId":
return <p className="text-black">{findParentName(cellValue)}</p>; return (
<p className="text-black">
{findParentName(masterUserLevel.parentLevelId)}
</p>
);
case "setup": case "setup":
return ( return (
@ -207,6 +222,12 @@ export default function MasterUserLevelTable() {
} }
/> />
); );
case "approvalActive":
return (
<p className="text-black">
{masterUserLevel.isApprovalActive ? "Yes" : "No"}
</p>
);
case "actions": case "actions":
return ( return (
<div className="relative flex justify-star items-center gap-2"> <div className="relative flex justify-star items-center gap-2">
@ -251,6 +272,16 @@ export default function MasterUserLevelTable() {
[selectedLevel, userLevelAll, masterUserLevelTable] [selectedLevel, userLevelAll, masterUserLevelTable]
); );
const saveApproval = async () => {
const req = { ids: selectedLevel.join(","), isApprovalActive: isSelected };
const res = await changeIsApproval(req);
if (res?.error) {
error(res?.message);
return false;
}
fetchData();
};
return ( return (
<> <>
<div className="mx-3 my-5"> <div className="mx-3 my-5">
@ -283,9 +314,18 @@ export default function MasterUserLevelTable() {
Settings Approval Settings Approval
</Button> </Button>
{doSetup && ( {doSetup && (
<Button color="success" className="text-white"> <div className="flex items-center gap-2">
Save Need Approval?
</Button> <Switch isSelected={isSelected} onValueChange={setIsSelected} />
{isSelected ? "Yes" : "No"}
<Button
color="success"
className="text-white"
onPress={saveApproval}
>
Save
</Button>
</div>
)} )}
</div> </div>

View File

@ -1,3 +1,11 @@
// next.config.js
const withPWA = require("next-pwa")({
dest: "public",
register: true,
skipWaiting: true,
// disable: process.env.NODE_ENV === "development", // disable PWA di mode dev
});
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
eslint: { eslint: {
@ -15,4 +23,4 @@ const nextConfig = {
}, },
}; };
module.exports = nextConfig; module.exports = withPWA(nextConfig);

2594
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,7 @@
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"next": "14.0.2", "next": "14.0.2",
"next-intl": "^3.26.0", "next-intl": "^3.26.0",
"next-pwa": "^5.6.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"postcss": "8.4.31", "postcss": "8.4.31",
"react": "18.2.0", "react": "18.2.0",

20
public/manifest.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "My PWA App",
"short_name": "PWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/divhumas.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/divhumas.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

101
public/sw.js Normal file
View File

@ -0,0 +1,101 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-e43f5367'], (function (workbox) { 'use strict';
importScripts();
self.skipWaiting();
workbox.clientsClaim();
workbox.registerRoute("/", new workbox.NetworkFirst({
"cacheName": "start-url",
plugins: [{
cacheWillUpdate: async ({
request,
response,
event,
state
}) => {
if (response && response.type === 'opaqueredirect') {
return new Response(response.body, {
status: 200,
statusText: 'OK',
headers: response.headers
});
}
return response;
}
}]
}), 'GET');
workbox.registerRoute(/.*/i, new workbox.NetworkOnly({
"cacheName": "dev",
plugins: []
}), 'GET');
}));
//# sourceMappingURL=sw.js.map

1
public/sw.js.map Normal file

File diff suppressed because one or more lines are too long

2456
public/workbox-e43f5367.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -30,7 +30,7 @@ export async function getAdvertise(data: any) {
}; };
const pathUrl = `/advertisement?page=${data?.page || 1}&limit=${ const pathUrl = `/advertisement?page=${data?.page || 1}&limit=${
data?.limit || "" data?.limit || ""
}&placement=${data?.placement || ""}`; }&placement=${data?.placement || ""}&isPublish=${data.isPublish || ""}`;
return await httpGet(pathUrl, headers); return await httpGet(pathUrl, headers);
} }

View File

@ -5,6 +5,10 @@ import {
httpPut, httpPut,
} from "@/service/http-config/axios-base-service"; } from "@/service/http-config/axios-base-service";
import Cookies from "js-cookie";
const token = Cookies.get("access_token");
export async function getAllUserLevels(data?: any) { export async function getAllUserLevels(data?: any) {
const headers = { const headers = {
"content-type": "application/json", "content-type": "application/json",
@ -43,3 +47,11 @@ export async function editUserLevels(id: string, request: any) {
}; };
return await httpPut(`user-levels/${id}`, headers, request); return await httpPut(`user-levels/${id}`, headers, request);
} }
export async function changeIsApproval(request: any) {
const headers = {
"content-type": "application/json",
Authorization: `Bearer ${token}`,
};
return await httpPut(`user-levels/enable-approval`, headers, request);
}