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 = {
|
const nextConfig: NextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
experimental: {},
|
experimental: {},
|
||||||
turbopack: { root: __dirname }
|
turbopack: { root: __dirname },
|
||||||
}
|
}
|
||||||
|
|
||||||
export default nextConfig
|
export default nextConfig
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "node scripts/generate-version.js && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
|
|||||||
21
public/sw.js
21
public/sw.js
@@ -1,5 +1,5 @@
|
|||||||
const CACHE_NAME = 'greenhome-v2';
|
const CACHE_NAME = 'greenhome-1766074058129';
|
||||||
const STATIC_CACHE_NAME = 'greenhome-static-v2';
|
const STATIC_CACHE_NAME = 'greenhome-static-1766074058129';
|
||||||
|
|
||||||
// Static assets to cache on install
|
// Static assets to cache on install
|
||||||
const STATIC_FILES_TO_CACHE = [
|
const STATIC_FILES_TO_CACHE = [
|
||||||
@@ -32,7 +32,8 @@ self.addEventListener('install', (event) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Some static files failed to cache:', 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) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
const { request } = event;
|
const { request } = event;
|
||||||
const url = new URL(request.url);
|
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 */}
|
{/* Tabs */}
|
||||||
<div className="bg-white rounded-2xl shadow-md border border-gray-200 overflow-hidden">
|
<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 => (
|
{TABS.map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
onClick={() => setActiveTab(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
|
activeTab === tab.value
|
||||||
? 'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50/50'
|
? 'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50/50'
|
||||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
||||||
@@ -497,8 +516,9 @@ function DailyReportContent() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-4 md:p-6 md:pt-0">
|
||||||
{/* Summary Tab */}
|
{/* Summary Tab */}
|
||||||
{activeTab === 'summary' && (
|
{activeTab === 'summary' && (
|
||||||
<SummaryTab
|
<SummaryTab
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
|
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
|
||||||
|
import UpdateNotification from '@/components/UpdateNotification'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'GreenHome',
|
title: 'GreenHome',
|
||||||
@@ -35,6 +36,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
|
|||||||
</head>
|
</head>
|
||||||
<body className='persian-number'>
|
<body className='persian-number'>
|
||||||
<ServiceWorkerRegistration />
|
<ServiceWorkerRegistration />
|
||||||
|
<UpdateNotification />
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,26 +8,51 @@ export default function ServiceWorkerRegistration() {
|
|||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
'serviceWorker' in navigator
|
'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 () => {
|
const registerServiceWorker = async () => {
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker.register('/sw.js', {
|
registration = await navigator.serviceWorker.register('/sw.js', {
|
||||||
scope: '/',
|
scope: '/',
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Service Worker registered successfully:', registration.scope);
|
console.log('Service Worker registered successfully:', registration.scope);
|
||||||
|
|
||||||
// Handle updates
|
// Listen for messages from service worker
|
||||||
registration.addEventListener('updatefound', () => {
|
navigator.serviceWorker.addEventListener('message', handleMessage);
|
||||||
const newWorker = registration.installing;
|
|
||||||
if (newWorker) {
|
// Check for updates periodically
|
||||||
newWorker.addEventListener('statechange', () => {
|
checkInterval = setInterval(() => {
|
||||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
if (registration) {
|
||||||
// New service worker available
|
registration.update();
|
||||||
console.log('New service worker available');
|
|
||||||
}
|
}
|
||||||
});
|
}, 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) {
|
} catch (error) {
|
||||||
console.error('Service Worker registration failed:', error);
|
console.error('Service Worker registration failed:', error);
|
||||||
}
|
}
|
||||||
@@ -39,6 +64,16 @@ export default function ServiceWorkerRegistration() {
|
|||||||
} else {
|
} else {
|
||||||
window.addEventListener('load', registerServiceWorker);
|
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':
|
case 'soil':
|
||||||
return <HumidityGauge value={currentValue} type="soil" />
|
return <HumidityGauge value={currentValue} type="soil" />
|
||||||
case 'lux':
|
case 'lux':
|
||||||
return <LuxGauge value={600} max={2000} />
|
return <LuxGauge value={currentValue} max={2000} />
|
||||||
case 'gas':
|
case 'gas':
|
||||||
return <GasGauge value={currentValue} max={100} />
|
return <GasGauge value={currentValue} max={100} />
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export type GreenhouseAlert = {
|
|||||||
|
|
||||||
export const TABS: { value: TabType; label: string }[] = [
|
export const TABS: { value: TabType; label: string }[] = [
|
||||||
{ value: 'summary', label: 'خلاصه' },
|
{ value: 'summary', label: 'خلاصه' },
|
||||||
{ value: 'charts', label: 'گزارش نموداری' },
|
{ value: 'charts', label: 'نمودار' },
|
||||||
{ value: 'weather', label: 'وضعیت آب و هوا' },
|
{ value: 'weather', label: 'آب و هوا' },
|
||||||
{ value: 'analysis', 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