Chapter 3: Other Features

Geolocation

Getting the position

import { useState } from 'react';
import { Button, Block } from '@cap-rel/smartcommon';

function LocationButton({ onLocation }) {
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);

    const getLocation = () => {
        if (!navigator.geolocation) {
            setError('Geolocation not supported');
            return;
        }

        setLoading(true);
        setError(null);

        navigator.geolocation.getCurrentPosition(
            (position) => {
                setLoading(false);
                onLocation({
                    lat: position.coords.latitude,
                    lng: position.coords.longitude,
                    accuracy: position.coords.accuracy
                });
            },
            (err) => {
                setLoading(false);
                setError(err.message);
            },
            {
                enableHighAccuracy: true,
                timeout: 10000,
                maximumAge: 0
            }
        );
    };

    return (
        <Block>
            <Button onClick={getLocation} loading={loading}>
                Get my position
            </Button>
            {error && <p className="text-red-500 mt-2">{error}</p>}
        </Block>
    );
}

SmartCommon Gps component

import { Gps } from '@cap-rel/smartcommon';

<Gps
    name="location"
    label="Intervention location"
    onChange={(coords) => console.log(coords)}
/>

File upload

PhotosUploader

import { PhotosUploader } from '@cap-rel/smartcommon';

<PhotosUploader
    name="photos"
    label="Photos"
    maxFiles={5}
    maxSize={5 * 1024 * 1024}  // 5 MB
    onChange={(files) => console.log(files)}
/>

FilesUploader

import { FilesUploader } from '@cap-rel/smartcommon';

<FilesUploader
    name="documents"
    label="Documents"
    accept=".pdf,.doc,.docx"
    multiple
    maxFiles={10}
/>

Resizing an image

import { useFile } from '@cap-rel/smartcommon';

function ImageUploader() {
    const { resizeImage } = useFile();

    const handleFile = async (e) => {
        const file = e.target.files[0];

        const base64 = await resizeImage(file, {
            maxWidth: 1200,
            maxHeight: 1200,
            quality: 80
        });

        // Send to the server
        await api.private.post('files', {
            json: {
                filename: file.name,
                content: base64
            }
        });
    };

    return <input type="file" accept="image/*" onChange={handleFile} />;
}

Dark theme

Global configuration

// appConfig.js
export const config = {
    globalState: {
        reducers: {
            settings: {
                theme: 'light'  // 'light' or 'dark'
            }
        }
    }
};

Applying the theme

import { useEffect } from 'react';
import { useGlobalStates } from '@cap-rel/smartcommon';

function ThemeProvider({ children }) {
    const gst = useGlobalStates();

    useEffect(() => {
        document.documentElement.classList.toggle(
            'dark',
            gst.get('settings')?.theme === 'dark'
        );
    }, [gst.get('settings')?.theme]);

    return children;
}

Theme toggle

import { useGlobalStates } from '@cap-rel/smartcommon';
import { Boolean } from '@cap-rel/smartcommon';

function ThemeToggle() {
    const gst = useGlobalStates();
    const settings = gst.get('settings');

    const toggleTheme = () => {
        gst.local.set('settings', {
            ...settings,
            theme: settings.theme === 'light' ? 'dark' : 'light'
        });
    };

    return (
        <Boolean
            value={settings?.theme === 'dark'}
            onChange={toggleTheme}
            label="Dark mode"
        />
    );
}

Handwritten signature

import { SignaturePad } from '@cap-rel/smartcommon';

<SignaturePad
    name="signature"
    label="Customer signature"
    width={400}
    height={200}
/>

Barcode scanning

Use a library such as @zxing/browser:

npm install @zxing/browser @zxing/library
import { useState, useRef, useEffect } from 'react';
import { BrowserMultiFormatReader } from '@zxing/browser';

function BarcodeScanner({ onScan }) {
    const videoRef = useRef(null);
    const [scanning, setScanning] = useState(false);

    useEffect(() => {
        let reader;

        if (scanning) {
            reader = new BrowserMultiFormatReader();
            reader.decodeFromVideoDevice(
                undefined,
                videoRef.current,
                (result, error) => {
                    if (result) {
                        onScan(result.getText());
                        setScanning(false);
                    }
                }
            );
        }

        return () => {
            if (reader) {
                reader.reset();
            }
        };
    }, [scanning]);

    return (
        <div>
            {scanning ? (
                <video ref={videoRef} className="w-full" />
            ) : (
                <button onClick={() => setScanning(true)}>
                    Scan a code
                </button>
            )}
        </div>
    );
}

Local notifications

function requestNotificationPermission() {
    if ('Notification' in window) {
        Notification.requestPermission().then(permission => {
            console.log('Permission:', permission);
        });
    }
}

function showNotification(title, body) {
    if (Notification.permission === 'granted') {
        new Notification(title, { body });
    }
}

Key takeaways

  1. Geolocation with navigator.geolocation or the Gps component
  2. Upload with PhotosUploader, FilesUploader, useFile
  3. Dark theme via useGlobalStates and CSS classes
  4. Signature with SignaturePad
  5. Integrate third-party libraries as needed

Previous Chapter | Back to Module | Next Module: Best Practices ->