upd: webpush
Deskripsi: - database push notification - update package - memasang webpush NO Issues
This commit is contained in:
@@ -55,6 +55,8 @@
|
|||||||
"rrule": "^2.8.1",
|
"rrule": "^2.8.1",
|
||||||
"supabase": "^1.192.5",
|
"supabase": "^1.192.5",
|
||||||
"web-push": "^3.6.7",
|
"web-push": "^3.6.7",
|
||||||
|
"wibu-cli": "^1.0.91",
|
||||||
|
"wibu-pkg": "^1.0.63",
|
||||||
"wibu-realtime": "bipproduction/wibu-realtime",
|
"wibu-realtime": "bipproduction/wibu-realtime",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
@@ -77,4 +79,4 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ model User {
|
|||||||
DivisionCalendarMember DivisionCalendarMember[]
|
DivisionCalendarMember DivisionCalendarMember[]
|
||||||
Notifications Notifications[] @relation("UserToUser")
|
Notifications Notifications[] @relation("UserToUser")
|
||||||
Notifications2 Notifications[] @relation("UserFromUser")
|
Notifications2 Notifications[] @relation("UserFromUser")
|
||||||
|
Subscribe Subscribe[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserLog {
|
model UserLog {
|
||||||
@@ -494,11 +495,6 @@ model BannerImage {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Subscription {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
data Json
|
|
||||||
}
|
|
||||||
|
|
||||||
model Notifications {
|
model Notifications {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
User1 User @relation("UserToUser", fields: [idUserTo], references: [id], map: "UserToUserMap")
|
User1 User @relation("UserToUser", fields: [idUserTo], references: [id], map: "UserToUserMap")
|
||||||
@@ -514,3 +510,12 @@ model Notifications {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Subscribe {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
User User @relation(fields: [idUser], references: [id])
|
||||||
|
idUser String @unique
|
||||||
|
subscription String @db.Text
|
||||||
|
createdAt DateTime? @default(now())
|
||||||
|
updatedAt DateTime? @updatedAt
|
||||||
|
}
|
||||||
BIN
public/icon-192x192.webp
Normal file
BIN
public/icon-192x192.webp
Normal file
Binary file not shown.
BIN
public/icon-512x512.webp
Normal file
BIN
public/icon-512x512.webp
Normal file
Binary file not shown.
121
public/wibu-push-worker.js
Normal file
121
public/wibu-push-worker.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
const log = false; // Ganti ke true untuk debugging
|
||||||
|
function printLog(text) {
|
||||||
|
if (log) {
|
||||||
|
const stack = new Error().stack;
|
||||||
|
const lineInfo = stack.split('\n')[2];
|
||||||
|
const match = lineInfo.match(/(\/.*:\d+:\d+)/);
|
||||||
|
const lineNumber = match ? match[1] : 'unknown line';
|
||||||
|
console.log(`[${lineNumber}] ==>`, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(self.skipWaiting());
|
||||||
|
printLog('Service Worker installing...');
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
printLog('Service Worker activating...');
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('push', async function (event) {
|
||||||
|
let title = "Default Title";
|
||||||
|
let options = {
|
||||||
|
body: "Default notification body",
|
||||||
|
icon: '/icon-192x192.png',
|
||||||
|
badge: '/icon-192x192.png',
|
||||||
|
image: '/icon-192x192.png',
|
||||||
|
vibrate: [100, 50, 100],
|
||||||
|
data: {
|
||||||
|
dateOfArrival: Date.now(),
|
||||||
|
primaryKey: '2',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.data) {
|
||||||
|
try {
|
||||||
|
const data = event.data.json();
|
||||||
|
title = data.title || title;
|
||||||
|
options.body = data.body || options.body;
|
||||||
|
options.data = {
|
||||||
|
...options.data,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
printLog(`Push event data: ${JSON.stringify(options, null, 2)}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing push event data:", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Push event has no data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const eventData = (options.data);
|
||||||
|
const clientList = await clients.matchAll({ type: 'window', includeUncontrolled: true });
|
||||||
|
let isClientFocused = false;
|
||||||
|
|
||||||
|
for (const client of clientList) {
|
||||||
|
client.postMessage({
|
||||||
|
type: 'PUSH_RECEIVED',
|
||||||
|
title: eventData.title,
|
||||||
|
body: eventData.body,
|
||||||
|
variant: eventData.variant,
|
||||||
|
createdAt: eventData.createdAt,
|
||||||
|
acceptedAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (client.focused) {
|
||||||
|
isClientFocused = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = await self.registration.pushManager.getSubscription();
|
||||||
|
const myEndpoint = subscription ? subscription.endpoint : null;
|
||||||
|
|
||||||
|
if (myEndpoint && eventData.endpoint === myEndpoint) {
|
||||||
|
printLog("Notification sent to self, skipping display.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.variant === 'data') {
|
||||||
|
printLog('Type is data, skipping display.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isClientFocused) {
|
||||||
|
await self.registration.showNotification(title, options);
|
||||||
|
} else {
|
||||||
|
printLog('Client is focused, notification not shown.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error displaying notification:", err);
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('notificationclick', function (event) {
|
||||||
|
const clickedLink = event.notification.data.link;
|
||||||
|
event.notification.close();
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
|
||||||
|
for (const client of clientList) {
|
||||||
|
if (client.url.includes(clickedLink) && 'focus' in client) {
|
||||||
|
return client.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (clients.openWindow) {
|
||||||
|
return clients.openWindow(clickedLink);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Error handling notification click:", err);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// wibu:1.0.87
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
|
||||||
event.waitUntil(self.skipWaiting());
|
|
||||||
console.log('Service worker installing...');
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
|
||||||
event.waitUntil(self.clients.claim());
|
|
||||||
console.log('Service worker activating...');
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('push', function (event) {
|
|
||||||
console.log('Push event received:', event);
|
|
||||||
|
|
||||||
|
|
||||||
let title = "Sistem Desa Mandiri";
|
|
||||||
let options = {
|
|
||||||
body: "Default notification body",
|
|
||||||
icon: '/icon-192x192.png',
|
|
||||||
badge: '/icon-192x192.png',
|
|
||||||
image: '/icon-192x192.png',
|
|
||||||
vibrate: [100, 50, 100],
|
|
||||||
data: {
|
|
||||||
dateOfArrival: Date.now(),
|
|
||||||
primaryKey: '2',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (event.data) {
|
|
||||||
try {
|
|
||||||
const data = event.data.json();
|
|
||||||
title = data.title || title;
|
|
||||||
options.body = data.body || options.body;
|
|
||||||
options.icon = data.icon || options.icon;
|
|
||||||
options.badge = data.badge || options.badge;
|
|
||||||
options.image = data.image || options.image;
|
|
||||||
options.data = {
|
|
||||||
...options.data,
|
|
||||||
...data.data, // Merging additional data from the event
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error parsing push event data:", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("Push event has no data.");
|
|
||||||
}
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
self.registration.showNotification(title, options)
|
|
||||||
.then(() => console.log('Notification shown.', JSON.stringify(options, null, 2)))
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Error showing notification:", err);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('notificationclick', function (event) {
|
|
||||||
console.log('Notification click received.');
|
|
||||||
|
|
||||||
event.notification.close(); // Close the notification
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
|
|
||||||
for (const client of clientList) {
|
|
||||||
if (client.url.includes('http://localhost:3005') && 'focus' in client) {
|
|
||||||
return client.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (clients.openWindow) {
|
|
||||||
return clients.openWindow('http://localhost:3005');
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
console.error("Error handling notification click:", err);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PushProvider } from "@/lib/PushProvider"
|
||||||
import { WrapLayout } from "@/module/_global"
|
import { WrapLayout } from "@/module/_global"
|
||||||
import { funDetectCookies, funGetUserByCookies } from "@/module/auth"
|
import { funDetectCookies, funGetUserByCookies } from "@/module/auth"
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
@@ -10,6 +11,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
|
|||||||
const user = await funGetUserByCookies()
|
const user = await funGetUserByCookies()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PushProvider user={String(user.id)} />
|
||||||
<WrapLayout role={user.idUserRole} theme={user.theme} user={user.id} village={user.idVillage}>
|
<WrapLayout role={user.idUserRole} theme={user.theme} user={user.id} village={user.idVillage}>
|
||||||
{children}
|
{children}
|
||||||
</WrapLayout>
|
</WrapLayout>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
const sub = await prisma.subscription.findMany();
|
|
||||||
return new Response(JSON.stringify({ data: sub }));
|
|
||||||
}
|
|
||||||
44
src/app/api/push-notification/route.ts
Normal file
44
src/app/api/push-notification/route.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { prisma } from "@/module/_global"
|
||||||
|
import { WibuServerPush } from 'wibu-pkg'
|
||||||
|
|
||||||
|
WibuServerPush.init({
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
|
||||||
|
VAPID_PRIVATE_KEY: process.env.VAPID_PRIVATE_KEY!,
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const { user, subscription } = await req.json()
|
||||||
|
const upsert = await prisma.subscribe.upsert({
|
||||||
|
where: {
|
||||||
|
idUser: user
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
idUser: user,
|
||||||
|
subscription: JSON.stringify(subscription)
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
subscription: JSON.stringify(subscription)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(upsert))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(req: Request) {
|
||||||
|
const sub = await prisma.subscribe.findMany()
|
||||||
|
const subs: PushSubscription[] = sub.map((v) => JSON.parse(v.subscription)) as PushSubscription[]
|
||||||
|
|
||||||
|
const kirim = await WibuServerPush.sendMany({
|
||||||
|
subscriptions: subs as any,
|
||||||
|
data: {
|
||||||
|
body: "ini test ",
|
||||||
|
title: "test notif",
|
||||||
|
link: "/",
|
||||||
|
variant: "notification"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(kirim))
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import webpush from "web-push";
|
|
||||||
import prisma from "@/lib/prisma";
|
|
||||||
|
|
||||||
// Set VAPID details for web-push
|
|
||||||
webpush.setVapidDetails(
|
|
||||||
"mailto:bip.production.js@gmail.com",
|
|
||||||
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
|
|
||||||
process.env.VAPID_PRIVATE_KEY!
|
|
||||||
);
|
|
||||||
|
|
||||||
export async function POST() {
|
|
||||||
try {
|
|
||||||
// Fetch all subscriptions from your database
|
|
||||||
const subscriptions = await prisma.subscription.findMany();
|
|
||||||
|
|
||||||
if (!subscriptions || subscriptions.length === 0) {
|
|
||||||
console.error("No subscriptions available to send notification");
|
|
||||||
return new Response("No subscriptions available", { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notification payload
|
|
||||||
const notificationPayload = JSON.stringify({
|
|
||||||
title: "Test Notification",
|
|
||||||
body: "This is a test notification | makuro",
|
|
||||||
icon: "/icon-192x192.png",
|
|
||||||
badge: "/icon-192x192.png",
|
|
||||||
image: "/icon-192x192.png",
|
|
||||||
});
|
|
||||||
|
|
||||||
let successCount = 0;
|
|
||||||
let failureCount = 0;
|
|
||||||
|
|
||||||
// Loop through all subscriptions and send notifications
|
|
||||||
for (const sub of subscriptions) {
|
|
||||||
try {
|
|
||||||
const subscriptionData = sub.data as any;
|
|
||||||
|
|
||||||
await webpush.sendNotification(subscriptionData, notificationPayload);
|
|
||||||
// console.log(
|
|
||||||
// `Notification sent successfully to ${subscriptionData.endpoint}`
|
|
||||||
// );
|
|
||||||
successCount++;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(
|
|
||||||
`Error sending push notification to subscription ${sub.id}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
failureCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a success or failure response
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
message: `Notifications sent: ${successCount}, Failed: ${failureCount}`,
|
|
||||||
success: failureCount === 0
|
|
||||||
}),
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error during notification process:", error);
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: error.message || "Failed to process notifications"
|
|
||||||
}),
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import webpush from "web-push";
|
|
||||||
import prisma from "@/lib/prisma";
|
|
||||||
webpush.setVapidDetails(
|
|
||||||
"mailto:bip.production.js@gmail.com",
|
|
||||||
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
|
|
||||||
process.env.VAPID_PRIVATE_KEY!
|
|
||||||
);
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
const { sub } = await req.json();
|
|
||||||
console.log(sub);
|
|
||||||
if (!sub || !sub.endpoint) {
|
|
||||||
console.error("Invalid subscription object");
|
|
||||||
return new Response("Invalid subscription object", { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await prisma.subscription.create({
|
|
||||||
data: {
|
|
||||||
id: sub.keys.auth,
|
|
||||||
data: sub
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Response(JSON.stringify({ data }));
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
const { sub } = await req.json();
|
|
||||||
const data = await prisma.subscription.delete({
|
|
||||||
where: {
|
|
||||||
id: sub.keys.auth
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return new Response(JSON.stringify({ data }));
|
|
||||||
}
|
|
||||||
39
src/app/icon.tsx
Normal file
39
src/app/icon.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { ImageResponse } from 'next/og'
|
||||||
|
|
||||||
|
// Image metadata
|
||||||
|
export const size = {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
}
|
||||||
|
export const contentType = 'image/png'
|
||||||
|
|
||||||
|
// Image generation
|
||||||
|
export default function Icon(): ImageResponse {
|
||||||
|
const { width, height } = size
|
||||||
|
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
// Element JSX yang berfungsi sebagai ikon
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
backgroundColor: 'black',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
B
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wibu:1.0.87
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
import "@mantine/core/styles.css";
|
import { ScrollProvider } from "@/module/_global";
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
ColorSchemeScript,
|
|
||||||
Container,
|
|
||||||
MantineProvider,
|
|
||||||
rem,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { ScrollProvider, WARNA } from "@/module/_global";
|
|
||||||
import { Lato } from "next/font/google";
|
|
||||||
import '@mantine/carousel/styles.css';
|
|
||||||
import { Toaster } from 'react-hot-toast';
|
|
||||||
import '@mantine/dates/styles.css';
|
|
||||||
import '@mantine/notifications/styles.css';
|
|
||||||
import { Notifications } from '@mantine/notifications'
|
|
||||||
import LayoutBackground from "@/module/_global/layout/layout_background";
|
import LayoutBackground from "@/module/_global/layout/layout_background";
|
||||||
|
import '@mantine/carousel/styles.css';
|
||||||
|
import { Box, ColorSchemeScript, MantineProvider } from "@mantine/core";
|
||||||
|
import "@mantine/core/styles.css";
|
||||||
|
import '@mantine/dates/styles.css';
|
||||||
|
import { Notifications } from '@mantine/notifications';
|
||||||
|
import '@mantine/notifications/styles.css';
|
||||||
|
import { Lato } from "next/font/google";
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "SISTEM DESA MANDIRI",
|
title: "SISTEM DESA MANDIRI",
|
||||||
|
|||||||
28
src/app/makuro/_lib/ButtonKirim.tsx
Normal file
28
src/app/makuro/_lib/ButtonKirim.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Button } from "@mantine/core";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function ButtonKirim() {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
async function onKirim() {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/makuro/api/kirim', {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataText = await res.text()
|
||||||
|
if (!res.ok) {
|
||||||
|
alert(dataText)
|
||||||
|
throw new Error(dataText)
|
||||||
|
}
|
||||||
|
const dataJson = JSON.parse(dataText)
|
||||||
|
console.log(dataJson)
|
||||||
|
alert("berhasil kirim")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <Button loading={loading} onClick={() => onKirim()} >Kirim</Button>
|
||||||
|
}
|
||||||
32
src/app/makuro/_lib/ButtonSubscribe.tsx
Normal file
32
src/app/makuro/_lib/ButtonSubscribe.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Button } from "@mantine/core";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function ButtonSubscribe({ user, subscription }: { user: string, subscription?: PushSubscription | null }) {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
async function subscribe() {
|
||||||
|
if(!subscription) return alert("no subscription")
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const res = await fetch('/makuro/api/sub', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ user, subscription })
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataText = await res.text()
|
||||||
|
if (!res.ok) {
|
||||||
|
alert(dataText)
|
||||||
|
throw new Error(dataText)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(dataText)
|
||||||
|
alert("berhasil subscribe")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <Button disabled={!subscription} onClick={() => subscribe()} loading={loading} variant="outline" radius={"xl"} size={"lg"}>Subscribe</Button>
|
||||||
|
}
|
||||||
5
src/app/makuro/_lib/MakuroProvider.tsx
Normal file
5
src/app/makuro/_lib/MakuroProvider.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { WibuPermissionProvider, WibuPushNotificationHandler } from "wibu-pkg";
|
||||||
|
|
||||||
|
export function MakuroProvider() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
7
src/app/makuro/_lib/UserSub.ts
Normal file
7
src/app/makuro/_lib/UserSub.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { PushSubscription } from 'web-push';
|
||||||
|
export class UserSub {
|
||||||
|
public static subscription: PushSubscription | null;
|
||||||
|
public static setSub(subscription: PushSubscription) {
|
||||||
|
this.subscription = subscription
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/app/makuro/_lib/state.ts
Normal file
3
src/app/makuro/_lib/state.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { hookstate } from "@hookstate/core";
|
||||||
|
|
||||||
|
export const subState = hookstate<PushSubscription | null>(null)
|
||||||
25
src/app/makuro/api/kirim/route.ts
Normal file
25
src/app/makuro/api/kirim/route.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { prisma } from "@/module/_global";
|
||||||
|
import { WibuServerPush } from 'wibu-pkg'
|
||||||
|
|
||||||
|
WibuServerPush.init({
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
|
||||||
|
VAPID_PRIVATE_KEY: process.env.VAPID_PRIVATE_KEY!,
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const sub = await prisma.subscribe.findMany()
|
||||||
|
const subs: PushSubscription[] = sub.map((v) => JSON.parse(v.subscription)) as PushSubscription[]
|
||||||
|
|
||||||
|
const kirim = await WibuServerPush.sendMany({
|
||||||
|
subscriptions: subs as any,
|
||||||
|
data: {
|
||||||
|
body: "ini test ",
|
||||||
|
title: "test notif",
|
||||||
|
link: "/",
|
||||||
|
variant: "notification"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(kirim))
|
||||||
|
|
||||||
|
}
|
||||||
21
src/app/makuro/api/sub/route.ts
Normal file
21
src/app/makuro/api/sub/route.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { prisma } from "@/module/_global"
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const { user, subscription } = await req.json()
|
||||||
|
console.log(user, subscription)
|
||||||
|
const upsert = await prisma.subscribe.upsert({
|
||||||
|
where: {
|
||||||
|
idUser: user
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
idUser: user,
|
||||||
|
subscription: JSON.stringify(subscription)
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
subscription: JSON.stringify(subscription)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(upsert))
|
||||||
|
|
||||||
|
}
|
||||||
22
src/app/makuro/kirim/page.tsx
Normal file
22
src/app/makuro/kirim/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
'use client'
|
||||||
|
import { useHookstate } from "@hookstate/core";
|
||||||
|
import { Card, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { subState } from "../_lib/state";
|
||||||
|
import { ButtonSubscribe } from "../_lib/ButtonSubscribe";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const user = useSearchParams().get("user")
|
||||||
|
const { value: sub } = useHookstate(subState)
|
||||||
|
if (!sub) return <Text>loading ...</Text>
|
||||||
|
if (!user) return <Text>masukkan user</Text>
|
||||||
|
return <Stack p={"md"} gap={"lg"}>
|
||||||
|
<Title>Kirim</Title>
|
||||||
|
<Card>
|
||||||
|
<Text>
|
||||||
|
{JSON.stringify(sub)}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
<ButtonSubscribe user={user} subscription={sub} />
|
||||||
|
</Stack>;
|
||||||
|
}
|
||||||
17
src/app/makuro/layout.tsx
Normal file
17
src/app/makuro/layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useHookstate } from "@hookstate/core"
|
||||||
|
import { WibuPermissionProvider, WibuPushNotificationHandler } from "wibu-pkg"
|
||||||
|
import { subState } from "./_lib/state"
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const { set: setSubcribe } = useHookstate(subState)
|
||||||
|
return <WibuPermissionProvider requiredPermissions={["notifications"]}>
|
||||||
|
<WibuPushNotificationHandler
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY={process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!}
|
||||||
|
onSubscribe={(sub) => {
|
||||||
|
setSubcribe(sub)
|
||||||
|
}} />
|
||||||
|
{children}
|
||||||
|
</WibuPermissionProvider>
|
||||||
|
}
|
||||||
24
src/app/makuro/terima/page.tsx
Normal file
24
src/app/makuro/terima/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
'use client'
|
||||||
|
import { useHookstate } from "@hookstate/core";
|
||||||
|
import { Card, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { subState } from "../_lib/state";
|
||||||
|
import { ButtonSubscribe } from "../_lib/ButtonSubscribe";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { ButtonKirim } from "../_lib/ButtonKirim";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const user = useSearchParams().get("user")
|
||||||
|
const { value: sub } = useHookstate(subState)
|
||||||
|
if (!sub) return <Text>loading ...</Text>
|
||||||
|
if (!user) return <Text>masukkan user</Text>
|
||||||
|
return <Stack p={"md"} gap={"lg"}>
|
||||||
|
<Title>terima</Title>
|
||||||
|
<Card>
|
||||||
|
<Text>
|
||||||
|
{JSON.stringify(sub)}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
<ButtonSubscribe user={user} subscription={sub} />
|
||||||
|
<ButtonKirim />
|
||||||
|
</Stack>;
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@ import type { MetadataRoute } from "next";
|
|||||||
|
|
||||||
export default function manifest(): MetadataRoute.Manifest {
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
return {
|
return {
|
||||||
name: "Sistem Desa Mandiri",
|
name: "Wibu Example",
|
||||||
short_name: "SDM",
|
short_name: "WibuExp",
|
||||||
description: "Sistem Desa Mandiri",
|
description: "Contoh penggunaan PWA dan push",
|
||||||
start_url: "/",
|
start_url: "/",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
background_color: "#ffffff",
|
background_color: "#ffffff",
|
||||||
@@ -13,17 +13,15 @@ export default function manifest(): MetadataRoute.Manifest {
|
|||||||
{
|
{
|
||||||
src: "/icon-192x192.png",
|
src: "/icon-192x192.png",
|
||||||
sizes: "192x192",
|
sizes: "192x192",
|
||||||
type: "image/png"
|
type: "image/png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/icon-512x512.png",
|
src: "/icon-512x512.png",
|
||||||
sizes: "512x512",
|
sizes: "512x512",
|
||||||
type: "image/png"
|
type: "image/png",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
serviceworker: {
|
|
||||||
src: "/wibu_worker.js"
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wibu:1.0.87
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { Button, Stack } from '@mantine/core'
|
|
||||||
import { useShallowEffect } from '@mantine/hooks'
|
|
||||||
import { useWibuRealtime } from 'wibu-realtime'
|
|
||||||
export function RealtimePage({ wibuKey }: { wibuKey: string }) {
|
|
||||||
const [data, setData] = useWibuRealtime({
|
|
||||||
WIBU_REALTIME_TOKEN: wibuKey,
|
|
||||||
project: "sdm"
|
|
||||||
})
|
|
||||||
useShallowEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
console.log(data)
|
|
||||||
}
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
async function onTekan() {
|
|
||||||
setData([{
|
|
||||||
idUserTo: 'supadminAmalia',
|
|
||||||
title: Math.random().toString(),
|
|
||||||
desc: 'Anda memiliki pengumuman baru. Silahkan periksa detailnya.',
|
|
||||||
category: 'announcement',
|
|
||||||
idContent:'cm1eg9fqh00019rhi3oqbej1i'
|
|
||||||
},{
|
|
||||||
idUserTo: 'supadmieenAmalia',
|
|
||||||
title: Math.random().toString(),
|
|
||||||
desc: 'Anda memiliki pengumuman baru. Silahkan periksa detailnya.',
|
|
||||||
category: 'announcement',
|
|
||||||
idContent:'dfdf'
|
|
||||||
}])
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Stack p={"lg"}>
|
|
||||||
{JSON.stringify(data)}
|
|
||||||
<Button onClick={onTekan}>Tekan</Button>
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Stack } from "@mantine/core";
|
|
||||||
import { RealtimePage } from "./_ui/RealtimePage";
|
|
||||||
const WIBU_REALTIME_KEY = process.env.WIBU_REALTIME_KEY!
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<RealtimePage wibuKey={WIBU_REALTIME_KEY} />
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
28
src/lib/PushProvider.tsx
Normal file
28
src/lib/PushProvider.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
'use client'
|
||||||
|
import { WibuPermissionProvider, WibuPushNotificationHandler } from 'wibu-pkg'
|
||||||
|
|
||||||
|
const NEXT_PUBLIC_VAPID_PUBLIC_KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!
|
||||||
|
export function PushProvider({ user }: { user: string }) {
|
||||||
|
if (!user) {
|
||||||
|
return <div>tunggu user</div>
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
<WibuPermissionProvider requiredPermissions={["notifications"]}>
|
||||||
|
<WibuPushNotificationHandler
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY={NEXT_PUBLIC_VAPID_PUBLIC_KEY}
|
||||||
|
onMessage={(msg) => {
|
||||||
|
console.log(msg)
|
||||||
|
}}
|
||||||
|
onSubscribe={(subscription) => {
|
||||||
|
fetch("/api/push-notification/", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
user,
|
||||||
|
subscription
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WibuPermissionProvider>
|
||||||
|
</>
|
||||||
|
}
|
||||||
32
src/opengraph-image.tsx
Normal file
32
src/opengraph-image.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ImageResponse } from "next/og";
|
||||||
|
|
||||||
|
export default async function Image() {
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "green",
|
||||||
|
color: "white",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
textAlign: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 128,
|
||||||
|
}}>WIBU APP</div>
|
||||||
|
<p style={{ fontSize: 24 }}>Comprehensive Dock for Wibu web app.</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
width: 1200,
|
||||||
|
height: 630
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// wibu:1.0.87
|
||||||
Reference in New Issue
Block a user