mirror of
https://github.com/usetrmnl/inker.git
synced 2026-04-29 13:45:07 -07:00
278 lines
11 KiB
TypeScript
278 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { MainLayout } from '../../components/layout';
|
|
import { Card, Button, Input } from '../../components/common';
|
|
import { useServerStatus } from '../../hooks/useServerStatus';
|
|
import { useMutation } from '../../hooks/useApi';
|
|
import { deviceService } from '../../services/api';
|
|
import type { DeviceFormData, Device } from '../../types';
|
|
|
|
/**
|
|
* Add Device page — setup instructions for TRMNL devices
|
|
* and alternative connection form for BYOD devices
|
|
*/
|
|
export function AddDevice() {
|
|
const navigate = useNavigate();
|
|
const { status } = useServerStatus();
|
|
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
// Alternative Connection form state
|
|
const [altDeviceName, setAltDeviceName] = useState('');
|
|
const [altMacAddress, setAltMacAddress] = useState('');
|
|
const [altWidth, setAltWidth] = useState('800');
|
|
const [altHeight, setAltHeight] = useState('480');
|
|
const [createdDevice, setCreatedDevice] = useState<Device | null>(null);
|
|
|
|
const { mutate: createDevice, isLoading: isCreatingDevice } = useMutation(
|
|
(data: DeviceFormData) => deviceService.create(data),
|
|
{
|
|
successMessage: 'Device registered successfully',
|
|
onSuccess: (newDevice) => {
|
|
setCreatedDevice(newDevice);
|
|
setAltDeviceName('');
|
|
setAltMacAddress('');
|
|
setAltWidth('800');
|
|
setAltHeight('480');
|
|
},
|
|
}
|
|
);
|
|
|
|
const handleAltSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setCreatedDevice(null);
|
|
await createDevice({
|
|
name: altDeviceName,
|
|
macAddress: altMacAddress,
|
|
width: parseInt(altWidth, 10) || 800,
|
|
height: parseInt(altHeight, 10) || 480,
|
|
});
|
|
};
|
|
|
|
// Build the full API URL that devices should use
|
|
const browserPort = typeof window !== 'undefined' ? window.location.port : '';
|
|
const deviceApiUrl = browserPort && browserPort !== '80'
|
|
? `http://${status.localIp}:${browserPort}`
|
|
: `http://${status.localIp}`;
|
|
|
|
const handleCopyUrl = async () => {
|
|
try {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(deviceApiUrl);
|
|
} else {
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = deviceApiUrl;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '-999999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
}
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
} catch (err) {
|
|
console.error('Failed to copy:', err);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<MainLayout>
|
|
<div className="max-w-2xl mx-auto space-y-6">
|
|
<div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigate('/devices')}
|
|
className="mb-4"
|
|
>
|
|
← Back to Devices
|
|
</Button>
|
|
<h1 className="text-3xl font-bold text-text-primary">Add Device</h1>
|
|
<p className="mt-2 text-sm text-text-secondary">
|
|
Connect a TRMNL device via WiFi setup or register a device manually.
|
|
</p>
|
|
</div>
|
|
|
|
{/* TRMNL Setup Instructions */}
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-text-primary mb-4">
|
|
Connect Your Device
|
|
</h2>
|
|
|
|
<div className="space-y-5">
|
|
{/* Status Warning */}
|
|
{!status.isOnline && (
|
|
<div className="p-4 bg-status-warning-bg border border-status-warning-border rounded-lg">
|
|
<p className="text-sm text-status-warning-text">
|
|
<strong>Warning:</strong> The server appears to be offline.
|
|
Devices will not be able to connect until it is running.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step-by-step Instructions */}
|
|
<div>
|
|
<h3 className="text-md font-semibold text-text-primary mb-3">
|
|
Setup Steps
|
|
</h3>
|
|
<ol className="space-y-3 list-decimal list-inside text-sm text-text-secondary">
|
|
<li>
|
|
<strong>Power on your TRMNL device</strong>
|
|
<p className="ml-6 mt-1 text-text-muted">
|
|
Hold the button until the screen activates. On first boot the device enters WiFi setup mode automatically.
|
|
</p>
|
|
</li>
|
|
<li>
|
|
<strong>Connect to the device's WiFi</strong>
|
|
<p className="ml-6 mt-1 text-text-muted">
|
|
On your phone or computer, join the network named <strong>TRMNL</strong> (or similar).
|
|
</p>
|
|
</li>
|
|
<li>
|
|
<strong>Open the captive portal</strong>
|
|
<p className="ml-6 mt-1 text-text-muted">
|
|
A setup page should open automatically. If not, open{' '}
|
|
<code className="bg-bg-muted px-1 rounded">192.168.4.1</code> in your browser.
|
|
</p>
|
|
</li>
|
|
<li>
|
|
<strong>Enter your WiFi credentials</strong>
|
|
<p className="ml-6 mt-1 text-text-muted">
|
|
Select your home/office WiFi and enter the password.
|
|
</p>
|
|
</li>
|
|
<li>
|
|
<strong>Set the Server URL in Advanced Settings</strong>
|
|
<p className="ml-6 mt-1 text-text-muted">
|
|
Open <strong>Advanced Settings</strong> in the captive portal and paste this URL into the <strong>API Server</strong> field:
|
|
</p>
|
|
<div className="ml-6 mt-2 flex items-center space-x-2">
|
|
<code className="flex-1 text-sm bg-bg-muted px-4 py-3 rounded-lg border border-border-default font-mono">
|
|
{deviceApiUrl}
|
|
</code>
|
|
<button
|
|
type="button"
|
|
onClick={handleCopyUrl}
|
|
className="inline-flex items-center justify-center px-3 py-1.5 text-sm font-semibold text-white rounded-xl transition-all duration-200"
|
|
style={{ backgroundColor: copied ? '#22c55e' : '#3b82f6' }}
|
|
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = copied ? '#16a34a' : '#2563eb'; }}
|
|
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = copied ? '#22c55e' : '#3b82f6'; }}
|
|
>
|
|
{copied ? 'Copied!' : 'Copy'}
|
|
</button>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<strong>Done!</strong>
|
|
<p className="ml-6 mt-1 text-text-muted">
|
|
The device will restart, connect to WiFi, and register with this server.
|
|
It will appear on the <strong>Devices</strong> page within a minute.
|
|
</p>
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<div className="p-3 bg-status-info-bg border border-status-info-border rounded-lg">
|
|
<p className="text-sm text-status-info-text">
|
|
<strong>Next step:</strong> Once the device appears, assign it to a playlist
|
|
so it starts displaying your screens.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Alternative Connection */}
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-text-primary mb-2">
|
|
Alternative Connection
|
|
</h2>
|
|
<p className="text-sm text-text-muted mb-4">
|
|
For devices that don't use the TRMNL WiFi setup flow (e.g., trmnl-android, custom ESP32 hardware).
|
|
Register a device by entering its name and MAC address.
|
|
</p>
|
|
|
|
{createdDevice ? (
|
|
<div className="p-4 bg-status-success-bg border border-status-success-border rounded-lg">
|
|
<p className="text-sm text-status-success-text mb-2">
|
|
<strong>Device registered!</strong> "{createdDevice.name}" has been added.
|
|
</p>
|
|
<div className="flex items-center gap-3">
|
|
<Link
|
|
to={`/devices/${createdDevice.id}`}
|
|
className="text-sm font-medium underline"
|
|
style={{ color: '#3b82f6' }}
|
|
>
|
|
View device
|
|
</Link>
|
|
<button
|
|
type="button"
|
|
onClick={() => setCreatedDevice(null)}
|
|
className="text-sm text-text-muted hover:text-text-secondary"
|
|
>
|
|
Add another
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleAltSubmit} className="space-y-4">
|
|
<Input
|
|
label="Device Name"
|
|
value={altDeviceName}
|
|
onChange={(e) => setAltDeviceName(e.target.value)}
|
|
placeholder="My Android Display"
|
|
required
|
|
/>
|
|
<Input
|
|
label="MAC Address"
|
|
value={altMacAddress}
|
|
onChange={(e) => setAltMacAddress(e.target.value)}
|
|
placeholder="AA:BB:CC:DD:EE:FF"
|
|
required
|
|
/>
|
|
|
|
{/* Screen Resolution */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-secondary mb-2">
|
|
Screen Resolution
|
|
</label>
|
|
<div className="flex gap-3">
|
|
<div className="flex-1">
|
|
<Input
|
|
label="Width (px)"
|
|
type="number"
|
|
value={altWidth}
|
|
onChange={(e) => setAltWidth(e.target.value)}
|
|
placeholder="800"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<Input
|
|
label="Height (px)"
|
|
type="number"
|
|
value={altHeight}
|
|
onChange={(e) => setAltHeight(e.target.value)}
|
|
placeholder="480"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p className="mt-1 text-xs text-text-muted">
|
|
Default is 800x480 (TRMNL Original). Enter your device's screen resolution in pixels.
|
|
</p>
|
|
</div>
|
|
|
|
<Button type="submit" isLoading={isCreatingDevice}>
|
|
Add Device
|
|
</Button>
|
|
</form>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
</MainLayout>
|
|
);
|
|
}
|