fix bug and version check
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 1s
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 1s
This commit is contained in:
@@ -3,7 +3,7 @@ import type { NextConfig } from 'next'
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
experimental: {},
|
||||
turbopack: { root: __dirname }
|
||||
turbopack: { root: __dirname },
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build": "node scripts/generate-version.js && next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
|
||||
21
public/sw.js
21
public/sw.js
@@ -1,5 +1,5 @@
|
||||
const CACHE_NAME = 'greenhome-v2';
|
||||
const STATIC_CACHE_NAME = 'greenhome-static-v2';
|
||||
const CACHE_NAME = 'greenhome-1766074058129';
|
||||
const STATIC_CACHE_NAME = 'greenhome-static-1766074058129';
|
||||
|
||||
// Static assets to cache on install
|
||||
const STATIC_FILES_TO_CACHE = [
|
||||
@@ -32,7 +32,8 @@ self.addEventListener('install', (event) => {
|
||||
} catch (error) {
|
||||
console.log('Some static files failed to cache:', error);
|
||||
}
|
||||
await self.skipWaiting();
|
||||
// Don't skip waiting automatically - let user decide when to update
|
||||
// await self.skipWaiting();
|
||||
})()
|
||||
);
|
||||
});
|
||||
@@ -53,6 +54,20 @@ self.addEventListener('activate', (event) => {
|
||||
);
|
||||
});
|
||||
|
||||
// Listen for messages from clients (e.g., when user clicks update button)
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting().then(() => {
|
||||
// Notify all clients about update
|
||||
return self.clients.matchAll();
|
||||
}).then((clients) => {
|
||||
clients.forEach((client) => {
|
||||
client.postMessage({ type: 'SW_UPDATED' });
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
3
public/version.json
Normal file
3
public/version.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": "1766074058129"
|
||||
}
|
||||
38
scripts/generate-version.js
Normal file
38
scripts/generate-version.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const version = Date.now().toString();
|
||||
const versionFile = path.join(__dirname, '../public/version.json');
|
||||
|
||||
// Ensure public directory exists
|
||||
const publicDir = path.dirname(versionFile);
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(versionFile, JSON.stringify({ version }, null, 2));
|
||||
|
||||
// Also write to .env.local for Next.js to use
|
||||
const envFile = path.join(__dirname, '../.env.local');
|
||||
const envContent = `NEXT_PUBLIC_APP_VERSION=${version}\n`;
|
||||
fs.writeFileSync(envFile, envContent);
|
||||
|
||||
// Update cache name in sw.js based on version
|
||||
const swPath = path.join(__dirname, '../public/sw.js');
|
||||
if (fs.existsSync(swPath)) {
|
||||
let swContent = fs.readFileSync(swPath, 'utf8');
|
||||
// Update cache names with version - using more specific regex
|
||||
const cacheNameRegex = /const CACHE_NAME\s*=\s*['"][^'"]*['"]/;
|
||||
const staticCacheNameRegex = /const STATIC_CACHE_NAME\s*=\s*['"][^'"]*['"]/;
|
||||
|
||||
if (cacheNameRegex.test(swContent)) {
|
||||
swContent = swContent.replace(cacheNameRegex, `const CACHE_NAME = 'greenhome-${version}'`);
|
||||
}
|
||||
if (staticCacheNameRegex.test(swContent)) {
|
||||
swContent = swContent.replace(staticCacheNameRegex, `const STATIC_CACHE_NAME = 'greenhome-static-${version}'`);
|
||||
}
|
||||
fs.writeFileSync(swPath, swContent);
|
||||
}
|
||||
|
||||
console.log(`Version generated: ${version}`);
|
||||
|
||||
@@ -482,12 +482,31 @@ function DailyReportContent() {
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="bg-white rounded-2xl shadow-md border border-gray-200 overflow-hidden">
|
||||
<div className="flex border-b border-gray-200 overflow-x-auto">
|
||||
{/* Segmented Control for Mobile */}
|
||||
<div className="p-3 md:p-6 md:pb-0">
|
||||
<div className="bg-gray-100 rounded-xl p-1 flex md:hidden">
|
||||
{TABS.map(tab => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => setActiveTab(tab.value)}
|
||||
className={`flex-1 min-w-[120px] px-6 py-4 text-sm font-medium transition-all duration-200 whitespace-nowrap ${
|
||||
className={`flex-1 px-2 py-2.5 text-xs font-medium rounded-lg transition-all duration-200 ${
|
||||
activeTab === tab.value
|
||||
? 'bg-white text-indigo-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Desktop Tabs */}
|
||||
<div className="hidden md:flex border-b border-gray-200 -mx-6 -mt-6 mb-6">
|
||||
{TABS.map(tab => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => setActiveTab(tab.value)}
|
||||
className={`flex-1 px-6 py-4 text-sm font-medium transition-all duration-200 whitespace-nowrap ${
|
||||
activeTab === tab.value
|
||||
? 'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50/50'
|
||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
||||
@@ -497,8 +516,9 @@ function DailyReportContent() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="p-4 md:p-6 md:pt-0">
|
||||
{/* Summary Tab */}
|
||||
{activeTab === 'summary' && (
|
||||
<SummaryTab
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
|
||||
import UpdateNotification from '@/components/UpdateNotification'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'GreenHome',
|
||||
@@ -35,6 +36,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
|
||||
</head>
|
||||
<body className='persian-number'>
|
||||
<ServiceWorkerRegistration />
|
||||
<UpdateNotification />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,26 +8,51 @@ export default function ServiceWorkerRegistration() {
|
||||
typeof window !== 'undefined' &&
|
||||
'serviceWorker' in navigator
|
||||
) {
|
||||
let registration: ServiceWorkerRegistration | null = null;
|
||||
let checkInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data && event.data.type === 'SW_UPDATED') {
|
||||
// Service worker has updated, reload the page
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (!document.hidden && registration) {
|
||||
registration.update();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
if (registration) {
|
||||
registration.update();
|
||||
}
|
||||
};
|
||||
|
||||
const registerServiceWorker = async () => {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('/sw.js', {
|
||||
registration = await navigator.serviceWorker.register('/sw.js', {
|
||||
scope: '/',
|
||||
});
|
||||
|
||||
console.log('Service Worker registered successfully:', registration.scope);
|
||||
|
||||
// Handle updates
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// New service worker available
|
||||
console.log('New service worker available');
|
||||
// Listen for messages from service worker
|
||||
navigator.serviceWorker.addEventListener('message', handleMessage);
|
||||
|
||||
// Check for updates periodically
|
||||
checkInterval = setInterval(() => {
|
||||
if (registration) {
|
||||
registration.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 60000); // Check every minute
|
||||
|
||||
// Check for updates when page becomes visible
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// Check for updates on page focus
|
||||
window.addEventListener('focus', handleFocus);
|
||||
} catch (error) {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
}
|
||||
@@ -39,6 +64,16 @@ export default function ServiceWorkerRegistration() {
|
||||
} else {
|
||||
window.addEventListener('load', registerServiceWorker);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
navigator.serviceWorker.removeEventListener('message', handleMessage);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
140
src/components/UpdateNotification.tsx
Normal file
140
src/components/UpdateNotification.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { RefreshCw, X } from 'lucide-react';
|
||||
|
||||
export default function UpdateNotification() {
|
||||
const [showUpdate, setShowUpdate] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let registration: ServiceWorkerRegistration | null = null;
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
try {
|
||||
registration = await navigator.serviceWorker.ready;
|
||||
|
||||
// Listen for service worker updates
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration?.installing;
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// New service worker available
|
||||
setShowUpdate(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check for updates every 60 seconds
|
||||
const checkInterval = setInterval(async () => {
|
||||
try {
|
||||
await registration?.update();
|
||||
} catch (error) {
|
||||
console.error('Error checking for updates:', error);
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
// Also check on page visibility change
|
||||
const handleVisibilityChange = () => {
|
||||
if (!document.hidden && registration) {
|
||||
registration.update();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// Check on page focus
|
||||
const handleFocus = () => {
|
||||
if (registration) {
|
||||
registration.update();
|
||||
}
|
||||
};
|
||||
window.addEventListener('focus', handleFocus);
|
||||
|
||||
return () => {
|
||||
clearInterval(checkInterval);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Service Worker registration error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
checkForUpdates();
|
||||
|
||||
// Check immediately on load
|
||||
navigator.serviceWorker.getRegistration().then(reg => {
|
||||
if (reg) {
|
||||
reg.update();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
setIsUpdating(true);
|
||||
|
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||
// Send message to service worker to skip waiting
|
||||
navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' });
|
||||
|
||||
// Reload the page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
setShowUpdate(false);
|
||||
};
|
||||
|
||||
if (!showUpdate) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-96 z-50 animate-in slide-in-from-bottom-5">
|
||||
<div className="bg-white rounded-xl shadow-lg border-2 border-indigo-500 p-4 flex items-center gap-3">
|
||||
<div className="flex-shrink-0 w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<RefreshCw className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-gray-900 text-sm">
|
||||
نسخه جدید آماده است
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 mt-0.5">
|
||||
برای دریافت آخرین تغییرات، اپ را بهروزرسانی کنید
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
disabled={isUpdating}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
{isUpdating ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
در حال بهروزرسانی...
|
||||
</>
|
||||
) : (
|
||||
'بهروزرسانی'
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
aria-label="بستن"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export function SummaryCard({ param, currentValue, minValue, maxValue, data }: S
|
||||
case 'soil':
|
||||
return <HumidityGauge value={currentValue} type="soil" />
|
||||
case 'lux':
|
||||
return <LuxGauge value={600} max={2000} />
|
||||
return <LuxGauge value={currentValue} max={2000} />
|
||||
case 'gas':
|
||||
return <GasGauge value={currentValue} max={100} />
|
||||
default:
|
||||
|
||||
@@ -36,8 +36,8 @@ export type GreenhouseAlert = {
|
||||
|
||||
export const TABS: { value: TabType; label: string }[] = [
|
||||
{ value: 'summary', label: 'خلاصه' },
|
||||
{ value: 'charts', label: 'گزارش نموداری' },
|
||||
{ value: 'weather', label: 'وضعیت آب و هوا' },
|
||||
{ value: 'charts', label: 'نمودار' },
|
||||
{ value: 'weather', label: 'آب و هوا' },
|
||||
{ value: 'analysis', label: 'تحلیل' },
|
||||
]
|
||||
|
||||
|
||||
12
src/lib/version.ts
Normal file
12
src/lib/version.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || Date.now().toString();
|
||||
|
||||
export async function getAppVersion(): Promise<string> {
|
||||
try {
|
||||
const response = await fetch('/version.json', { cache: 'no-store' });
|
||||
const data = await response.json();
|
||||
return data.version;
|
||||
} catch {
|
||||
return APP_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user