Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions niffler-ng-client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8"/>
<link rel="icon" href="/favicon.ico"/>
<link rel="manifest" href="manifest.json"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Niffler</title>
</head>
Expand Down
69 changes: 69 additions & 0 deletions niffler-ng-client/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"name": "Niffler. The coin keeper",
"short_name": "Niffler",
"description": "The coin keeper App. Pet project",
"start_url": "/main",
"scope": "/",
"display": "standalone",
"id": "/",
"theme_color": "black",
"background_color": "white",
"icons": [
{
"src": "/pwa/launchericon-48-48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "/pwa/launchericon-72-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/pwa/launchericon-96-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/pwa/launchericon-144-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/pwa/launchericon-192-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/pwa/launchericon-512-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/pwa/launchericon-512-512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"screenshots": [
{
"form_factor": "narrow",
"sizes": "712x1498",
"src": "/pwa/stats-narrow.png",
"type": "image/png"
},
{
"form_factor": "narrow",
"sizes": "712x1498",
"src": "/pwa/profile-narrow.png",
"type": "image/png"
},
{
"form_factor": "wide",
"sizes": "2924x1712",
"src": "/pwa/stats-wide.png",
"type": "image/png"
}
]
}
105 changes: 89 additions & 16 deletions niffler-ng-client/public/firebase-messaging-sw.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
importScripts('https://www.gstatic.com/firebasejs/12.2.1/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/12.2.1/firebase-messaging-compat.js');

const CASHE_NAME = "pwa-cache-v1";
const OFFLINE_URL = "/offline.html";
const NIFFLER_IMAGE = "/pwa/launchericon-512-512.png";

self.skipWaiting();
self.addEventListener("activate", event => {
event.waitUntil(clients.claim());
});

firebase.initializeApp({
apiKey: "AIzaSyDEtu5oECQq5s8PU--l22YZbt8ck-fB9sI",
authDomain: "niffler-ea54f.firebaseapp.com",
Expand All @@ -13,23 +22,87 @@ firebase.initializeApp({
const messaging = firebase.messaging();

messaging.onBackgroundMessage((payload) => {
const { title, body, icon, click_action } = payload.notification ?? {};
self.registration.showNotification(title || 'Notification', {
body: body || '',
icon: icon || '/icon.png',
data: { click_action: click_action || '/' }
});
console.log("fcm")
const {title, body, icon, click_action} = payload.notification ?? {};
console.log(payload.notification);
self.registration.showNotification(title || 'Notification', {
body: body || '',
icon: icon || '/icon.png',
data: {click_action: click_action || '/'}
});
});

self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification?.data?.click_action || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
for (const client of clientList) {
if ('focus' in client && client.url.includes(self.origin)) return client.focus();
}
return clients.openWindow(url);
})
);
event.notification.close();
const url = event.notification?.data?.click_action || '/';
event.waitUntil(
clients.matchAll({type: 'window', includeUncontrolled: true}).then((clientList) => {
for (const client of clientList) {
if ('focus' in client && client.url.includes(self.origin)) return client.focus();
}
return clients.openWindow(url);
})
);
});

self.addEventListener("install", event => {
event.waitUntil(
caches.open(CASHE_NAME).then(cache => {
return cache.addAll([OFFLINE_URL, NIFFLER_IMAGE]);
})
)
});

self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
event.respondWith((async () => {
if (!navigator.onLine) {
const cache = await caches.open(CASHE_NAME);
return cache.match(OFFLINE_URL);
}
return fetch(event.request);
})());
}

if (event.request.destination === "image") {
event.respondWith((async () => {
const cache = await caches.open(CASHE_NAME);
const cachedResponse = await cache.match(event.request);
if (cachedResponse) return cachedResponse;

return await fetch(event.request);
})());
}
});

self.addEventListener("push", function (event) {
console.log("push handler");
const data = event.data.json();
console.log(data);
const {title, body, interaction} = data.notification ?? {};

const options = {
body: body || '',
icon: '/pwa/launchericon-512x512.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now()
},
actions: [
{
action: 'confirm',
title: 'OK'
},
{
action: 'close',
title: 'Close notification'
},
],
requireInteraction: interaction
};

event.waitUntil(
self.registration.showNotification(title, options)
);
});

42 changes: 42 additions & 0 deletions niffler-ng-client/public/offline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Niffler.Offline</title>
<style>
html, body {
height: 100%;
margin: 0;
}

body {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-family: Arial, sans-serif;
background-color: #f5f5f5;
}

main {
max-width: 90%;
}

h1 {
font-size: 2em;
margin: 0.5em 0;
}

p {
font-size: 1em;
color: #555;
}
</style>
</head>
<body>
<main>
<h1 data-testid="offline-title">You're currently offline</h1>
<img alt="Niffler image" src="/pwa/launchericon-512-512.png" width="200" height="200"/>.
<p>Try to reload a little bit later</p>
</main>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added niffler-ng-client/public/pwa/profile-narrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added niffler-ng-client/public/pwa/stats-narrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added niffler-ng-client/public/pwa/stats-wide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions niffler-ng-client/src/components/AppContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const AppContent: FC = () => {
<Route path="/people">
<Route path="/people/all" element={<PeoplePage activeTab={"all"}/>}/>
<Route path="/people/friends" element={<PeoplePage activeTab={"friends"}/>}/>
<Route path="/people/invite" element={<PeoplePage activeTab={"invite"}/>}/>
</Route>
<Route path="*" element={<NotFoundPage/>}/>
</Route>
Expand Down
139 changes: 139 additions & 0 deletions niffler-ng-client/src/components/AvatarCapture/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {FC, RefObject, useEffect, useRef} from "react";
import {Box, Fade, Modal} from "@mui/material";
import {PrimaryButton, SecondaryButton} from "../Button";
import "./styles.css";

interface AvatarCaptureInterface {
inputRef: RefObject<HTMLInputElement> | null,
isOpen: boolean;
onCapture?: (file: File) => void,
onClose?: () => void,
}

export const AvatarCapture: FC<AvatarCaptureInterface> = ({inputRef, isOpen, onCapture, onClose}) => {
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
let stream: MediaStream | null;

const closeCamera = () => {
if(stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
if(videoRef.current) {
videoRef.current.srcObject = null;
videoRef.current.load();
}
}


useEffect(() => {
const startCamera = async() => {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {facingMode: "user"}
});
if (videoRef.current){
videoRef.current.srcObject = stream;
}
} catch (err) {
console.error("Camera error:", err);
}
}

startCamera();

return () => {
closeCamera();
}
}, []);

const handleCapture = () => {
if(!videoRef.current || !canvasRef.current || !inputRef?.current) return;

const video = videoRef.current;
const canvas = canvasRef.current;

canvas.width = video.videoWidth;
canvas.height = video.videoHeight;

const ctx = canvas.getContext("2d");
if(!ctx) return;

ctx.drawImage(video, 0, 0 , canvas.width, canvas.height);

canvas.toBlob(blob => {
if(!blob) return;

const file = new File([blob], "avatar.jpg", {type: "image/jpeg"});

const dt = new DataTransfer();
dt.items.add(file);

inputRef.current!.files = dt.files;

if(onCapture) {
onCapture(file);
}
}, "image/jpeg");
}


return (
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={isOpen}
onClose={() => {
closeCamera();
if(onClose) {
onClose();
}
}}
closeAfterTransition
>
<Fade in={isOpen}>
<Box
width={"90%"}
height={"90%"}
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
margin: "50px auto",
position: "relative",
}}
>
<video
data-testid={"avatar-capture-video"}
className={"avatar-capture__video"}
ref={videoRef}
autoPlay
playsInline
width={"100%"}
height={"100%"}
/>
<canvas ref={canvasRef} className="avatar-capture__canvas"/>
<Box sx={{
position: "absolute",
bottom: 0,
margin: 2,
}}>
<PrimaryButton
data-testid={"capture-avatar-button"}
sx={{marginRight: 2}}
onClick={handleCapture}>Capture Avatar</PrimaryButton>
<SecondaryButton
data-testid={"dismiss-photo-button"}
onClick={() => {
closeCamera();
if(onClose) {
onClose();
}
}}>Dismiss</SecondaryButton>
</Box>
</Box>
</Fade>
</Modal>
)
}
Loading
Loading