This commit is contained in:
2025-01-17 14:35:01 -05:00
parent d0c8e3988a
commit 8e5b0ef2ba
2 changed files with 253 additions and 354 deletions

View File

@@ -1,18 +1,12 @@
// src/App.js
import React, { useState, useEffect, useRef } from 'react';
// Replace with your actual public VAPID key
const VAPID_PUBLIC_KEY = 'BCn73fh1YZV3rFbK9H234he4nNWhhEKnYiQ_UZ1U0nxR6Q6cKvTG6v05Uyq7KXh0FxwjPOV3jHR3DPLqF5Lyfm4';
// REPLACE with your public VAPID key from "web-push generate-vapid-keys"
const VAPID_PUBLIC_KEY = 'BGTl7xYXEr2gY_O6gmVGYy0DTFlm6vepYUmkt8_6P9PHwOJcHsPZ5CUSEzsoCq7CszPwMyUbq0nG6xjrzJMWZOg';
function App() {
// -----------------------------
// BASIC STATE & MESSAGING
// -----------------------------
const [statusMsg, setStatusMsg] = useState('');
// -----------------------------
// SERVER TIMER STATE
// -----------------------------
// Timer state
const [soloUsed, setSoloUsed] = useState([]);
const [togetherUsed, setTogetherUsed] = useState([]);
const [logs, setLogs] = useState([]);
@@ -21,41 +15,35 @@ function App() {
const [isPaused, setIsPaused] = useState(false);
const [ringing, setRinging] = useState(false);
// For managing “Movie” (4 points) or “Episode” (2 points)
// Movie/Episode selection
const [chooseMovieMode, setChooseMovieMode] = useState(false);
const [chooseEpisodeMode, setChooseEpisodeMode] = useState(false);
const [chosenPoints, setChosenPoints] = useState([]); // e.g. [{ category:'solo', index:2 }, ...]
const [chosenPoints, setChosenPoints] = useState([]);
// For the multi-step “Add Points flow
// Add Points flow
const [showAddPointsOverlay, setShowAddPointsOverlay] = useState(false);
const [addPointsStep, setAddPointsStep] = useState(1); // 1 => password, 2 => category, 3 => amount
const [addPointsStep, setAddPointsStep] = useState(1);
const [pointsPassword, setPointsPassword] = useState('');
const [pointsCategory, setPointsCategory] = useState('');
const [pointsAmount, setPointsAmount] = useState(1);
// Admin mode
const [adminMode, setAdminMode] = useState(false);
// Audio ref for local alarm (only if the tab is foreground)
// Local "alarm" audio (only if page is foreground)
const alarmRef = useRef(null);
// -----------------------------
// ON MOUNT: FETCH STATE
// & REGISTER PUSH
// -----------------------------
useEffect(() => {
// Poll the server for timer state
// Poll server state
fetchState();
const intervalId = setInterval(fetchState, 1000);
return () => clearInterval(intervalId);
const id = setInterval(fetchState, 1000);
return () => clearInterval(id);
}, []);
useEffect(() => {
// Register service worker + request push
registerSWAndSubscribeToPush();
// Register SW + push
registerServiceWorkerAndSubscribe();
}, []);
// If ringing changes, try to play local alarm (if tab is foreground)
useEffect(() => {
if (ringing) {
playLocalAlarm();
@@ -64,12 +52,9 @@ function App() {
}
}, [ringing]);
// -----------------------------
// FUNCTIONS
// -----------------------------
async function fetchState() {
try {
const res = await fetch('/state');
const res = await fetch('/state'); // same origin => server on port 80
const data = await res.json();
setSoloUsed(data.soloUsed || []);
setTogetherUsed(data.togetherUsed || []);
@@ -84,13 +69,12 @@ function App() {
}
}
// ----- Local alarm (only if foreground) -----
function playLocalAlarm() {
if (!alarmRef.current) return;
alarmRef.current.loop = true;
alarmRef.current.currentTime = 0;
alarmRef.current.play().catch((err) => {
console.log('Alarm play might be blocked until user interacts:', err);
alarmRef.current.play().catch(err => {
console.log('Audio play blocked until user interaction:', err);
});
}
function stopLocalAlarm() {
@@ -100,7 +84,6 @@ function App() {
alarmRef.current.loop = false;
}
// ----- Single bubble => 15-min -----
async function handleBubbleClick(category, index) {
try {
const res = await fetch('/use-bubble', {
@@ -112,7 +95,7 @@ function App() {
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg(`Using 1 ${category} bubble => 15-min timer started`);
setStatusMsg(`Using 1 ${category} => 15-min started`);
}
} catch (err) {
console.error(err);
@@ -120,7 +103,6 @@ function App() {
}
}
// ----- Pause/Resume -----
async function handlePauseResume() {
try {
const res = await fetch('/pause-resume', { method: 'POST' });
@@ -132,11 +114,10 @@ function App() {
}
} catch (err) {
console.error(err);
setStatusMsg('Error: pause/resume');
setStatusMsg('Error pause/resume');
}
}
// ----- Finish Up (2 min) -----
async function handleFinishUp() {
try {
const res = await fetch('/finish-up', { method: 'POST' });
@@ -144,15 +125,13 @@ function App() {
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('2-minute finish-up started');
setStatusMsg('2-min finish-up started');
}
} catch (err) {
console.error(err);
setStatusMsg('Error finishing up');
}
}
// ----- Cancel Finish-Up -----
async function handleCancelFinishUp() {
try {
const res = await fetch('/cancel-finish-up', { method: 'POST' });
@@ -164,11 +143,9 @@ function App() {
}
} catch (err) {
console.error(err);
setStatusMsg('Error canceling finish-up');
setStatusMsg('Error cancel finish-up');
}
}
// ----- Ignore ring -----
async function handleIgnoreRing() {
try {
const res = await fetch('/ignore-ring', { method: 'POST' });
@@ -176,7 +153,7 @@ function App() {
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('Ring ignored, no 2-min started');
setStatusMsg('Ring ignored');
}
} catch (err) {
console.error(err);
@@ -184,7 +161,7 @@ function App() {
}
}
// ----- Movie/Episode usage -----
// Movie/Episode
function startChooseMovie() {
setChooseMovieMode(true);
setChooseEpisodeMode(false);
@@ -198,33 +175,31 @@ function App() {
setStatusMsg('Select exactly 2 points for an Episode');
}
function toggleChosen(category, index) {
const found = chosenPoints.find(
(c) => c.category === category && c.index === index
);
const found = chosenPoints.find(c => c.category===category && c.index===index);
if (found) {
setChosenPoints(chosenPoints.filter((c) => c !== found));
setChosenPoints(chosenPoints.filter(c => c !== found));
} else {
setChosenPoints([...chosenPoints, { category, index }]);
}
}
async function submitMovie() {
if (chosenPoints.length !== 4) {
if (chosenPoints.length!==4) {
setStatusMsg('Must pick exactly 4 points for a movie');
return;
}
try {
const res = await fetch('/use-movie', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chosenPoints }),
const res=await fetch('/use-movie',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ chosenPoints }),
});
const data = await res.json();
if (data.error) {
const data=await res.json();
if(data.error){
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('Movie => used chosen 4 points');
}
} catch (err) {
} catch(err){
console.error(err);
setStatusMsg('Error submitting movie');
}
@@ -232,23 +207,23 @@ function App() {
setChosenPoints([]);
}
async function submitEpisode() {
if (chosenPoints.length !== 2) {
setStatusMsg('Must pick exactly 2 points for an Episode');
if(chosenPoints.length!==2){
setStatusMsg('Must pick exactly 2 points for an episode');
return;
}
try {
const res = await fetch('/use-episode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chosenPoints }),
const res=await fetch('/use-episode',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ chosenPoints })
});
const data = await res.json();
if (data.error) {
const data=await res.json();
if(data.error){
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('Episode => used chosen 2 points');
}
} catch (err) {
} catch(err){
console.error(err);
setStatusMsg('Error submitting episode');
}
@@ -256,7 +231,7 @@ function App() {
setChosenPoints([]);
}
// ----- Add Points (3-step flow) -----
// Add Points flow
function openAddPointsFlow() {
setShowAddPointsOverlay(true);
setAddPointsStep(1);
@@ -267,186 +242,171 @@ function App() {
function closeAddPointsFlow() {
setShowAddPointsOverlay(false);
}
// Step 1: check password immediately
// Step 1 => check password
async function checkPointsPassword() {
if (!pointsPassword) {
if(!pointsPassword){
setStatusMsg('Please enter a password');
return;
}
try {
const res = await fetch('/check-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pointsPassword }),
const res=await fetch('/check-password',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ password:pointsPassword })
});
const data = await res.json();
if (data.error) {
const data=await res.json();
if(data.error){
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('Password ok, pick category next');
setStatusMsg('Password OK, pick category next');
setAddPointsStep(2);
}
} catch (err) {
} catch(err){
console.error(err);
setStatusMsg('Error checking password');
}
}
// Step 2 => pick category
function pickAddPointsCategory(cat) {
function pickAddPointsCategory(cat){
setPointsCategory(cat);
setAddPointsStep(3);
}
// Step 3 => pick amount => do /add-points
async function pickAddPointsAmount(amount) {
setPointsAmount(amount);
// Step 3 => pick amount => /add-points
async function pickAddPointsAmount(amt){
setPointsAmount(amt);
try {
const res = await fetch('/add-points', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
const res=await fetch('/add-points',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({
password: pointsPassword,
category: pointsCategory,
amount,
}),
amount: amt
})
});
const data = await res.json();
if (data.error) {
const data=await res.json();
if(data.error){
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg(`+${amount} points added to ${pointsCategory}`);
setStatusMsg(`+${amt} points added to ${pointsCategory}`);
}
} catch (err) {
} catch(err){
console.error(err);
setStatusMsg('Error adding points');
}
setShowAddPointsOverlay(false);
}
// ----- ADMIN -----
async function handleAdminLogin() {
const pw = prompt('Enter admin password:');
if (!pw) return;
// Admin
async function handleAdminLogin(){
const pw=prompt('Enter admin password:');
if(!pw) return;
try {
const res = await fetch('/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pw }),
const res=await fetch('/admin/login',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ password:pw })
});
const data = await res.json();
if (data.error) {
const data=await res.json();
if(data.error){
setStatusMsg(`Error: ${data.error}`);
} else {
setAdminMode(true);
setStatusMsg('Admin mode enabled');
}
} catch (err) {
} catch(err){
console.error(err);
setStatusMsg('Error admin login');
}
}
function handleAdminLogout() {
function handleAdminLogout(){
setAdminMode(false);
setStatusMsg('Admin mode disabled');
}
async function adminRequest(path) {
async function adminRequest(path){
try {
const res = await fetch(`/admin/${path}`, { method: 'POST' });
const data = await res.json();
if (data.error) {
const res=await fetch(`/admin/${path}`,{ method:'POST'});
const data=await res.json();
if(data.error){
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg(`Admin => ${path} success`);
}
} catch (err) {
} catch(err){
console.error(err);
setStatusMsg(`Error admin ${path}`);
}
}
// -----------------------------
// PUSH NOTIFICATIONS SETUP
// -----------------------------
async function registerSWAndSubscribeToPush() {
if (!('serviceWorker' in navigator)) {
console.log('Service workers not supported in this browser.');
// Push subscription
async function registerServiceWorkerAndSubscribe(){
if(!('serviceWorker' in navigator)){
console.log('No service worker support in this browser');
return;
}
try {
// Register custom service worker from /public folder
const reg = await navigator.serviceWorker.register('/service-worker.js');
console.log('Service Worker registered:', reg);
// Request Notification permission
// Request notifications
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.log('Notification permission not granted.');
if(permission!=='granted'){
console.log('User denied notifications');
return;
}
// Subscribe
const subscription = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
userVisibleOnly:true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
console.log('Push subscription:', subscription);
// Send subscription to server
await fetch('/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subscription }),
// Send to server
await fetch('/subscribe',{
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ subscription })
});
console.log('Subscribed to push notifications!');
} catch (err) {
console.error('SW registration or push subscription failed:', err);
console.log('Subscribed to push!');
} catch(err){
console.error('SW or push subscription failed:', err);
}
}
function urlBase64ToUint8Array(base64String) {
function urlBase64ToUint8Array(base64String){
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i=0; i<rawData.length; i++){
outputArray[i] = rawData.charCodeAt(i);
const base64 = (base64String+padding).replace(/\-/g,'+').replace(/_/g,'/');
const rawData=window.atob(base64);
const outputArray=new Uint8Array(rawData.length);
for(let i=0;i<rawData.length;i++){
outputArray[i]=rawData.charCodeAt(i);
}
return outputArray;
}
// -----------------------------
// RENDER BUBBLES (solo/together)
// -----------------------------
function renderBubbles(usedArr, category) {
return usedArr.map((val, idx) => {
const isSelected = chosenPoints.find(
(c) => c.category === category && c.index === idx
);
const bubbleColor = val ? '#8B0000' : '#4caf50'; // used => dark red, unused => green
// Outline color for base vs appended
let outline = (idx < 5) ? '2px solid #5f7b99' : '2px solid #7f5f99';
if (isSelected) outline = '3px solid yellow';
// Render Bubbles
function renderBubbles(usedArr, category){
return usedArr.map((val,idx)=>{
const isChosen = chosenPoints.find(c=> c.category===category && c.index===idx);
const bubbleColor = val? '#8B0000':'#4caf50'; // used => dark red, unused => green
let outline = (idx<5)? '2px solid #5f7b99':'2px solid #7f5f99'; // base vs appended
if(isChosen) outline='3px solid yellow';
return (
<div
key={idx}
style={{
width: 30,
height: 30,
borderRadius: '50%',
width:30, height:30, borderRadius:'50%',
backgroundColor: bubbleColor,
margin: 5,
cursor: 'pointer',
border: outline,
boxSizing: 'border-box',
margin:5, cursor:'pointer',
border: outline, boxSizing:'border-box'
}}
onClick={() => {
if (val) {
setStatusMsg('That bubble is already used.');
onClick={()=>{
if(val){
setStatusMsg('That bubble is already used');
return;
}
if (chooseMovieMode || chooseEpisodeMode) {
if(chooseMovieMode||chooseEpisodeMode){
toggleChosen(category, idx);
} else {
handleBubbleClick(category, idx);
@@ -457,10 +417,9 @@ function App() {
});
}
// Are we in the 2-min finish-up”?
// Are we in the 2-min finish-up
const inFinishUp = timerRunning && !ringing && timeRemaining>0 && timeRemaining<=120000;
// Format time
function formatTime(ms){
if(ms<=0) return '00:00';
const totalSec=Math.floor(ms/1000);
@@ -468,14 +427,13 @@ function App() {
const ss=totalSec%60;
return `${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`;
}
// Format logs
function formatLogDate(iso){
const d=new Date(iso);
const w=d.toLocaleDateString('en-US',{ weekday:'long' });
const dayOfWeek=d.toLocaleDateString('en-US',{ weekday:'long'});
const day=d.getDate();
const ord=getOrdinal(day);
const timeStr=d.toLocaleTimeString('en-US',{ hour12:true,hour:'numeric',minute:'2-digit' });
return `${w} ${day}${ord}, ${timeStr}`;
const timeStr=d.toLocaleTimeString('en-US',{ hour12:true,hour:'numeric',minute:'2-digit'});
return `${dayOfWeek} ${day}${ord}, ${timeStr}`;
}
function getOrdinal(n){
const s=['th','st','nd','rd'];
@@ -483,87 +441,82 @@ function App() {
return s[(v-20)%10]||s[v]||s[0];
}
// -----------------------------
// RENDER
// -----------------------------
return (
<div style={styles.container}>
<h1>Screen-Time App (Push Notifications)</h1>
<h1>Screen-Time on Port 80</h1>
{statusMsg && <div style={styles.statusBox}>{statusMsg}</div>}
{statusMsg && (
<div style={styles.statusBox}>{statusMsg}</div>
)}
{/* Audio for local ring (foreground only) */}
<audio ref={alarmRef} src="/alarm.mp3" />
{/* SOLO */}
<section>
<h2>Solo Points</h2>
<div style={styles.bubbleRow}>
{renderBubbles(soloUsed,'solo')}
{renderBubbles(soloUsed, 'solo')}
</div>
</section>
{/* TOGETHER */}
<section>
<h2>Together Points</h2>
<div style={styles.bubbleRow}>
{renderBubbles(togetherUsed,'together')}
{renderBubbles(togetherUsed, 'together')}
</div>
</section>
{/* Add Points (3-step) */}
<button style={styles.button} onClick={openAddPointsFlow}>
Add Points
</button>
{/* MOVIE / EPISODE */}
<section style={{marginTop:20}}>
{chooseMovieMode?(
<div>
<p>Select 4 points for a Movie</p>
<p>Select exactly 4 points => Movie</p>
<button style={styles.button} onClick={submitMovie}>Submit Movie</button>
<button style={styles.button} onClick={()=> {
<button style={styles.button} onClick={()=>{
setChooseMovieMode(false);
setChosenPoints([]);
}}>Cancel</button>
</div>
):(
<button style={styles.button} onClick={startChooseMovie}>
Movie (4 points)
Movie (4)
</button>
)}
{chooseEpisodeMode?(
<div>
<p>Select 2 points for an Episode</p>
<p>Select exactly 2 points => Episode</p>
<button style={styles.button} onClick={submitEpisode}>Submit Episode</button>
<button style={styles.button} onClick={()=> {
<button style={styles.button} onClick={()=>{
setChooseEpisodeMode(false);
setChosenPoints([]);
}}>Cancel</button>
</div>
):(
<button style={styles.button} onClick={startChooseEpisode}>
Episode (2 points)
Episode (2)
</button>
)}
</section>
{/* Timer display */}
<section style={{marginTop:30}}>
<section style={{ marginTop:30 }}>
{(timerRunning || timeRemaining>0) && (
<div>
<h3>Timer: {formatTime(timeRemaining)} {isPaused && '(Paused)'}</h3>
<button style={styles.button} onClick={handlePauseResume}>
{isPaused? 'Resume' : 'Pause'}
{isPaused? 'Resume':'Pause'}
</button>
</div>
)}
{ringing && (
<div style={{marginTop:10}}>
<p style={{color:'yellow'}}>Timer ended! Alarm is ringing!</p>
<button style={styles.button} onClick={handleFinishUp}>Finish Up (2 min)</button>
<button style={styles.button} onClick={handleIgnoreRing}>Ignore</button>
<button style={styles.button} onClick={handleFinishUp}>
Finish Up (2 min)
</button>
<button style={styles.button} onClick={handleIgnoreRing}>
Ignore
</button>
</div>
)}
{inFinishUp && (
@@ -575,10 +528,9 @@ function App() {
)}
</section>
{/* Logs */}
<section style={{marginTop:20}}>
<h3>Recent Logs (last 5)</h3>
{logs.slice(-5).reverse().map((entry, i)=>(
{logs.slice(-5).reverse().map((entry,i)=>(
<p key={i}>
[{entry.type.toUpperCase()}]
{entry.index!=null && ` #${entry.index+1}`}
@@ -587,7 +539,6 @@ function App() {
))}
</section>
{/* Add Points Overlay */}
{showAddPointsOverlay && (
<div style={styles.overlay}>
<div style={styles.addPointsDialog}>
@@ -603,10 +554,7 @@ function App() {
<button style={styles.button} onClick={checkPointsPassword}>
Check Password
</button>
<button
style={{...styles.button, backgroundColor:'#999'}}
onClick={closeAddPointsFlow}
>
<button style={{...styles.button, backgroundColor:'#999'}} onClick={closeAddPointsFlow}>
Cancel
</button>
</>
@@ -615,17 +563,10 @@ function App() {
<>
<h3>Step 2: Choose Category</h3>
<div style={styles.buttonRow}>
<button style={styles.button} onClick={()=>pickAddPointsCategory('solo')}>
Solo
</button>
<button style={styles.button} onClick={()=>pickAddPointsCategory('together')}>
Together
</button>
<button style={styles.button} onClick={()=> pickAddPointsCategory('solo')}>Solo</button>
<button style={styles.button} onClick={()=> pickAddPointsCategory('together')}>Together</button>
</div>
<button
style={{...styles.button, backgroundColor:'#999'}}
onClick={closeAddPointsFlow}
>
<button style={{...styles.button, backgroundColor:'#999'}} onClick={closeAddPointsFlow}>
Cancel
</button>
</>
@@ -634,20 +575,17 @@ function App() {
<>
<h3>Step 3: How many points?</h3>
<div style={styles.buttonRow}>
{[1,2,3,4].map(amt => (
{[1,2,3,4].map(n=>(
<button
key={amt}
key={n}
style={styles.button}
onClick={()=>pickAddPointsAmount(amt)}
onClick={()=> pickAddPointsAmount(n)}
>
+{amt}
+{n}
</button>
))}
</div>
<button
style={{...styles.button, backgroundColor:'#999'}}
onClick={closeAddPointsFlow}
>
<button style={{...styles.button, backgroundColor:'#999'}} onClick={closeAddPointsFlow}>
Cancel
</button>
</>
@@ -656,22 +594,17 @@ function App() {
</div>
)}
{/* Admin Panel */}
{adminMode?(
<div style={styles.adminPanel}>
<h2>Admin Panel</h2>
<button style={styles.adminButton} onClick={()=>adminRequest('clear-logs')}>Clear Logs</button>
<button style={styles.adminButton} onClick={()=>adminRequest('reset-usage')}>Reset Usage</button>
<button style={styles.adminButton} onClick={()=>adminRequest('remove-all-earned')}>
Remove All Earned
</button>
<button style={styles.adminButton} onClick={()=>adminRequest('remove-all-earned')}>Remove All Earned</button>
<button style={styles.adminButton} onClick={()=>adminRequest('add-earned')}>+1 (SoloUsed)</button>
<button style={styles.adminButton} onClick={()=>adminRequest('remove-earned')}>-1 (SoloUsed)</button>
<button style={styles.adminButton} onClick={()=>adminRequest('clear-timer')}>Clear Timer</button>
<button style={styles.adminButton} onClick={()=>adminRequest('expire-timer')}>Expire Timer (1s)</button>
<button style={styles.adminButton} onClick={handleAdminLogout}>
Logout Admin
</button>
<button style={styles.adminButton} onClick={handleAdminLogout}>Logout Admin</button>
</div>
):(
<div style={{marginTop:30}}>
@@ -684,9 +617,6 @@ function App() {
);
}
// -----------------------------
// STYLES
// -----------------------------
const styles = {
container: {
maxWidth:800,