Spaces:
Runtime error
Runtime error
PWA
Browse filesSigned-off-by: Jintao Zhang <zhangjintao9020@gmail.com>
- index.html +6 -2
- public/icons/icon-192x192.png +1 -0
- public/icons/icon-512x512.png +1 -0
- public/icons/icon-512x512.svg +6 -0
- public/manifest.json +59 -0
- public/sw.js +71 -0
- scripts/generate-icons.js +41 -0
- src/App.jsx +21 -2
- src/index.css +90 -0
- src/main.jsx +5 -1
- src/serviceWorkerRegistration.js +134 -0
index.html
CHANGED
|
@@ -2,8 +2,12 @@
|
|
| 2 |
<html lang="zh-CN">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>Thinking Model Client</title>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
@@ -12,4 +16,4 @@
|
|
| 12 |
<div id="root"></div>
|
| 13 |
<script type="module" src="/src/main.jsx"></script>
|
| 14 |
</body>
|
| 15 |
-
</html>
|
|
|
|
| 2 |
<html lang="zh-CN">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
| 6 |
<title>Thinking Model Client</title>
|
| 7 |
+
<meta name="description" content="A client for interacting with thinking models" />
|
| 8 |
+
<meta name="theme-color" content="#3e6ae1" />
|
| 9 |
+
<link rel="manifest" href="/manifest.json" />
|
| 10 |
+
<link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
|
| 11 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 12 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 13 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
|
|
| 16 |
<div id="root"></div>
|
| 17 |
<script type="module" src="/src/main.jsx"></script>
|
| 18 |
</body>
|
| 19 |
+
</html>
|
public/icons/icon-192x192.png
ADDED
|
|
public/icons/icon-512x512.png
ADDED
|
|
public/icons/icon-512x512.svg
ADDED
|
|
public/manifest.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Thinking Model Client",
|
| 3 |
+
"short_name": "Thinking Model",
|
| 4 |
+
"description": "A client for interacting with thinking models",
|
| 5 |
+
"start_url": "/",
|
| 6 |
+
"display": "standalone",
|
| 7 |
+
"background_color": "#ffffff",
|
| 8 |
+
"theme_color": "#3e6ae1",
|
| 9 |
+
"icons": [
|
| 10 |
+
{
|
| 11 |
+
"src": "icons/icon-72x72.png",
|
| 12 |
+
"sizes": "72x72",
|
| 13 |
+
"type": "image/png",
|
| 14 |
+
"purpose": "any maskable"
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"src": "icons/icon-96x96.png",
|
| 18 |
+
"sizes": "96x96",
|
| 19 |
+
"type": "image/png",
|
| 20 |
+
"purpose": "any maskable"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"src": "icons/icon-128x128.png",
|
| 24 |
+
"sizes": "128x128",
|
| 25 |
+
"type": "image/png",
|
| 26 |
+
"purpose": "any maskable"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"src": "icons/icon-144x144.png",
|
| 30 |
+
"sizes": "144x144",
|
| 31 |
+
"type": "image/png",
|
| 32 |
+
"purpose": "any maskable"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"src": "icons/icon-152x152.png",
|
| 36 |
+
"sizes": "152x152",
|
| 37 |
+
"type": "image/png",
|
| 38 |
+
"purpose": "any maskable"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"src": "icons/icon-192x192.png",
|
| 42 |
+
"sizes": "192x192",
|
| 43 |
+
"type": "image/png",
|
| 44 |
+
"purpose": "any maskable"
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"src": "icons/icon-384x384.png",
|
| 48 |
+
"sizes": "384x384",
|
| 49 |
+
"type": "image/png",
|
| 50 |
+
"purpose": "any maskable"
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"src": "icons/icon-512x512.png",
|
| 54 |
+
"sizes": "512x512",
|
| 55 |
+
"type": "image/png",
|
| 56 |
+
"purpose": "any maskable"
|
| 57 |
+
}
|
| 58 |
+
]
|
| 59 |
+
}
|
public/sw.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Service Worker for Thinking Model Client PWA
|
| 2 |
+
|
| 3 |
+
const CACHE_NAME = 'thinking-model-cache-v1';
|
| 4 |
+
const urlsToCache = [
|
| 5 |
+
'/',
|
| 6 |
+
'/index.html',
|
| 7 |
+
'/manifest.json',
|
| 8 |
+
'/icons/icon-192x192.png',
|
| 9 |
+
'/icons/icon-512x512.png'
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
// Install event - cache basic assets
|
| 13 |
+
self.addEventListener('install', event => {
|
| 14 |
+
event.waitUntil(
|
| 15 |
+
caches.open(CACHE_NAME)
|
| 16 |
+
.then(cache => {
|
| 17 |
+
console.log('Opened cache');
|
| 18 |
+
return cache.addAll(urlsToCache);
|
| 19 |
+
})
|
| 20 |
+
);
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
// Activate event - clean up old caches
|
| 24 |
+
self.addEventListener('activate', event => {
|
| 25 |
+
const cacheWhitelist = [CACHE_NAME];
|
| 26 |
+
event.waitUntil(
|
| 27 |
+
caches.keys().then(cacheNames => {
|
| 28 |
+
return Promise.all(
|
| 29 |
+
cacheNames.map(cacheName => {
|
| 30 |
+
if (cacheWhitelist.indexOf(cacheName) === -1) {
|
| 31 |
+
return caches.delete(cacheName);
|
| 32 |
+
}
|
| 33 |
+
})
|
| 34 |
+
);
|
| 35 |
+
})
|
| 36 |
+
);
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// Fetch event - serve from cache if available, otherwise fetch from network
|
| 40 |
+
self.addEventListener('fetch', event => {
|
| 41 |
+
event.respondWith(
|
| 42 |
+
caches.match(event.request)
|
| 43 |
+
.then(response => {
|
| 44 |
+
// Cache hit - return response
|
| 45 |
+
if (response) {
|
| 46 |
+
return response;
|
| 47 |
+
}
|
| 48 |
+
return fetch(event.request).then(
|
| 49 |
+
response => {
|
| 50 |
+
// Check if we received a valid response
|
| 51 |
+
if (!response || response.status !== 200 || response.type !== 'basic') {
|
| 52 |
+
return response;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Clone the response
|
| 56 |
+
const responseToCache = response.clone();
|
| 57 |
+
|
| 58 |
+
caches.open(CACHE_NAME)
|
| 59 |
+
.then(cache => {
|
| 60 |
+
// Don't cache API requests
|
| 61 |
+
if (!event.request.url.includes('/api/')) {
|
| 62 |
+
cache.put(event.request, responseToCache);
|
| 63 |
+
}
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
return response;
|
| 67 |
+
}
|
| 68 |
+
);
|
| 69 |
+
})
|
| 70 |
+
);
|
| 71 |
+
});
|
scripts/generate-icons.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// This script can be used to generate different sized icons from the SVG source
|
| 2 |
+
// You would need to install sharp: npm install sharp
|
| 3 |
+
// Then run: node scripts/generate-icons.js
|
| 4 |
+
|
| 5 |
+
const fs = require('fs');
|
| 6 |
+
const path = require('path');
|
| 7 |
+
const sharp = require('sharp');
|
| 8 |
+
|
| 9 |
+
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
| 10 |
+
const svgPath = path.join(__dirname, '../public/icons/icon-512x512.svg');
|
| 11 |
+
const outputDir = path.join(__dirname, '../public/icons');
|
| 12 |
+
|
| 13 |
+
async function generateIcons() {
|
| 14 |
+
try {
|
| 15 |
+
// Make sure the output directory exists
|
| 16 |
+
if (!fs.existsSync(outputDir)) {
|
| 17 |
+
fs.mkdirSync(outputDir, { recursive: true });
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Read the SVG file
|
| 21 |
+
const svgBuffer = fs.readFileSync(svgPath);
|
| 22 |
+
|
| 23 |
+
// Generate each size
|
| 24 |
+
for (const size of sizes) {
|
| 25 |
+
const outputPath = path.join(outputDir, `icon-${size}x${size}.png`);
|
| 26 |
+
|
| 27 |
+
await sharp(svgBuffer)
|
| 28 |
+
.resize(size, size)
|
| 29 |
+
.png()
|
| 30 |
+
.toFile(outputPath);
|
| 31 |
+
|
| 32 |
+
console.log(`Generated: ${outputPath}`);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
console.log('All icons generated successfully!');
|
| 36 |
+
} catch (error) {
|
| 37 |
+
console.error('Error generating icons:', error);
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
generateIcons();
|
src/App.jsx
CHANGED
|
@@ -32,6 +32,7 @@ function App() {
|
|
| 32 |
const [showSettings, setShowSettings] = useState(false);
|
| 33 |
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
| 34 |
const [showProfileDropdown, setShowProfileDropdown] = useState(false);
|
|
|
|
| 35 |
|
| 36 |
const activeProfile = profiles.find(p => p.id === activeProfileId) || profiles[0];
|
| 37 |
|
|
@@ -58,6 +59,18 @@ function App() {
|
|
| 58 |
|
| 59 |
const toggleSidebar = () => {
|
| 60 |
setSidebarCollapsed(!sidebarCollapsed);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
};
|
| 62 |
|
| 63 |
const toggleProfileDropdown = () => {
|
|
@@ -72,7 +85,13 @@ function App() {
|
|
| 72 |
return (
|
| 73 |
<div className="flex flex-col h-screen w-full overflow-hidden">
|
| 74 |
<div className="flex justify-between items-center px-5 h-header border-b border-border bg-background">
|
| 75 |
-
<div className="flex items-center">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
<h1 className="text-xl font-semibold text-text">Thinking Model Client</h1>
|
| 77 |
</div>
|
| 78 |
<div className="flex items-center gap-2.5">
|
|
@@ -107,7 +126,7 @@ function App() {
|
|
| 107 |
</div>
|
| 108 |
|
| 109 |
<div className="flex flex-1 overflow-hidden">
|
| 110 |
-
<div className={`
|
| 111 |
<div className="flex justify-between items-center py-3 px-4 border-b border-border">
|
| 112 |
<h2 className="text-sm font-semibold text-light-text m-0">Conversations</h2>
|
| 113 |
<button
|
|
|
|
| 32 |
const [showSettings, setShowSettings] = useState(false);
|
| 33 |
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
| 34 |
const [showProfileDropdown, setShowProfileDropdown] = useState(false);
|
| 35 |
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
| 36 |
|
| 37 |
const activeProfile = profiles.find(p => p.id === activeProfileId) || profiles[0];
|
| 38 |
|
|
|
|
| 59 |
|
| 60 |
const toggleSidebar = () => {
|
| 61 |
setSidebarCollapsed(!sidebarCollapsed);
|
| 62 |
+
// 在移动设备上,如果侧边栏是展开的,点击收起按钮也应该关闭移动菜单
|
| 63 |
+
if (window.innerWidth <= 768 && !sidebarCollapsed) {
|
| 64 |
+
setMobileMenuOpen(false);
|
| 65 |
+
}
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
const toggleMobileMenu = () => {
|
| 69 |
+
setMobileMenuOpen(!mobileMenuOpen);
|
| 70 |
+
// 如果侧边栏是收起的,则展开它
|
| 71 |
+
if (sidebarCollapsed) {
|
| 72 |
+
setSidebarCollapsed(false);
|
| 73 |
+
}
|
| 74 |
};
|
| 75 |
|
| 76 |
const toggleProfileDropdown = () => {
|
|
|
|
| 85 |
return (
|
| 86 |
<div className="flex flex-col h-screen w-full overflow-hidden">
|
| 87 |
<div className="flex justify-between items-center px-5 h-header border-b border-border bg-background">
|
| 88 |
+
<div className="flex items-center gap-2">
|
| 89 |
+
<button
|
| 90 |
+
className="md:hidden bg-transparent border-0 text-base text-lightest-text cursor-pointer flex items-center justify-center z-10 w-8 h-8 hover:text-text"
|
| 91 |
+
onClick={toggleMobileMenu}
|
| 92 |
+
>
|
| 93 |
+
☰
|
| 94 |
+
</button>
|
| 95 |
<h1 className="text-xl font-semibold text-text">Thinking Model Client</h1>
|
| 96 |
</div>
|
| 97 |
<div className="flex items-center gap-2.5">
|
|
|
|
| 126 |
</div>
|
| 127 |
|
| 128 |
<div className="flex flex-1 overflow-hidden">
|
| 129 |
+
<div className={`sidebar ${sidebarCollapsed ? 'collapsed' : ''} ${mobileMenuOpen ? 'mobile-open' : ''}`}>
|
| 130 |
<div className="flex justify-between items-center py-3 px-4 border-b border-border">
|
| 131 |
<h2 className="text-sm font-semibold text-light-text m-0">Conversations</h2>
|
| 132 |
<button
|
src/index.css
CHANGED
|
@@ -20,6 +20,10 @@
|
|
| 20 |
--assistant-message-color: #1a1a1a;
|
| 21 |
--hover-color: #f5f5f5;
|
| 22 |
--active-color: #e6f2ff;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
* {
|
|
@@ -549,3 +553,89 @@ body {
|
|
| 549 |
@apply bg-gray-400;
|
| 550 |
}
|
| 551 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
--assistant-message-color: #1a1a1a;
|
| 21 |
--hover-color: #f5f5f5;
|
| 22 |
--active-color: #e6f2ff;
|
| 23 |
+
|
| 24 |
+
/* Responsive sidebar width */
|
| 25 |
+
--mobile-sidebar-width: 100%;
|
| 26 |
+
--tablet-sidebar-width: 250px;
|
| 27 |
}
|
| 28 |
|
| 29 |
* {
|
|
|
|
| 553 |
@apply bg-gray-400;
|
| 554 |
}
|
| 555 |
}
|
| 556 |
+
|
| 557 |
+
/* Responsive styles */
|
| 558 |
+
@media (max-width: 768px) {
|
| 559 |
+
.app-header {
|
| 560 |
+
padding: 0 1rem;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
.app-title {
|
| 564 |
+
font-size: 1rem;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.sidebar {
|
| 568 |
+
position: fixed;
|
| 569 |
+
z-index: 50;
|
| 570 |
+
height: 100%;
|
| 571 |
+
width: 100%;
|
| 572 |
+
left: -100%;
|
| 573 |
+
transition: all 0.3s ease-in-out;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.sidebar.mobile-open {
|
| 577 |
+
left: 0;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
/* 确保在移动设备上收起的侧边栏仍然可见 */
|
| 581 |
+
.sidebar.collapsed {
|
| 582 |
+
width: 50px;
|
| 583 |
+
left: 0;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.message-content {
|
| 587 |
+
max-width: 100%;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
.settings-modal {
|
| 591 |
+
width: 95%;
|
| 592 |
+
max-height: 80vh;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.profiles-list {
|
| 596 |
+
flex-direction: column;
|
| 597 |
+
gap: 0.5rem;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.chat-input {
|
| 601 |
+
padding: 0.75rem;
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
.message-input {
|
| 605 |
+
font-size: 0.875rem;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.send-button {
|
| 609 |
+
padding: 0 1rem;
|
| 610 |
+
}
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
/* Small mobile devices */
|
| 614 |
+
@media (max-width: 480px) {
|
| 615 |
+
.app-header {
|
| 616 |
+
height: 50px;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
.right-section {
|
| 620 |
+
gap: 0.5rem;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
.profile-dropdown-button,
|
| 624 |
+
.settings-button {
|
| 625 |
+
padding: 0.25rem 0.5rem;
|
| 626 |
+
font-size: 0.75rem;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.chat-messages {
|
| 630 |
+
padding: 0.75rem;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
.message-content {
|
| 634 |
+
padding: 0.75rem;
|
| 635 |
+
font-size: 0.875rem;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
.welcome-screen h2 {
|
| 639 |
+
font-size: 1.25rem;
|
| 640 |
+
}
|
| 641 |
+
}
|
src/main.jsx
CHANGED
|
@@ -3,9 +3,13 @@ import ReactDOM from 'react-dom/client'
|
|
| 3 |
import App from './App'
|
| 4 |
import './tailwind.css'
|
| 5 |
import './index.css'
|
|
|
|
| 6 |
|
| 7 |
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 8 |
<React.StrictMode>
|
| 9 |
<App />
|
| 10 |
</React.StrictMode>,
|
| 11 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import App from './App'
|
| 4 |
import './tailwind.css'
|
| 5 |
import './index.css'
|
| 6 |
+
import { register as registerServiceWorker } from './serviceWorkerRegistration'
|
| 7 |
|
| 8 |
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 9 |
<React.StrictMode>
|
| 10 |
<App />
|
| 11 |
</React.StrictMode>,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
// Register service worker for PWA functionality
|
| 15 |
+
registerServiceWorker()
|
src/serviceWorkerRegistration.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// This optional code is used to register a service worker.
|
| 2 |
+
// register() is not called by default.
|
| 3 |
+
|
| 4 |
+
// This lets the app load faster on subsequent visits in production, and gives
|
| 5 |
+
// it offline capabilities. However, it also means that developers (and users)
|
| 6 |
+
// will only see deployed updates on subsequent visits to a page, after all the
|
| 7 |
+
// existing tabs open on the page have been closed, since previously cached
|
| 8 |
+
// resources are updated in the background.
|
| 9 |
+
|
| 10 |
+
const isLocalhost = Boolean(
|
| 11 |
+
window.location.hostname === 'localhost' ||
|
| 12 |
+
// [::1] is the IPv6 localhost address.
|
| 13 |
+
window.location.hostname === '[::1]' ||
|
| 14 |
+
// 127.0.0.0/8 are considered localhost for IPv4.
|
| 15 |
+
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
| 16 |
+
);
|
| 17 |
+
|
| 18 |
+
export function register(config) {
|
| 19 |
+
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
| 20 |
+
// The URL constructor is available in all browsers that support SW.
|
| 21 |
+
const publicUrl = new URL(import.meta.env.BASE_URL, window.location.href);
|
| 22 |
+
if (publicUrl.origin !== window.location.origin) {
|
| 23 |
+
// Our service worker won't work if BASE_URL is on a different origin
|
| 24 |
+
// from what our page is served on. This might happen if a CDN is used to
|
| 25 |
+
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
| 26 |
+
return;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
window.addEventListener('load', () => {
|
| 30 |
+
const swUrl = `${import.meta.env.BASE_URL}sw.js`;
|
| 31 |
+
|
| 32 |
+
if (isLocalhost) {
|
| 33 |
+
// This is running on localhost. Let's check if a service worker still exists or not.
|
| 34 |
+
checkValidServiceWorker(swUrl, config);
|
| 35 |
+
|
| 36 |
+
// Add some additional logging to localhost, pointing developers to the
|
| 37 |
+
// service worker/PWA documentation.
|
| 38 |
+
navigator.serviceWorker.ready.then(() => {
|
| 39 |
+
console.log(
|
| 40 |
+
'This web app is being served cache-first by a service ' +
|
| 41 |
+
'worker. To learn more, visit https://cra.link/PWA'
|
| 42 |
+
);
|
| 43 |
+
});
|
| 44 |
+
} else {
|
| 45 |
+
// Is not localhost. Just register service worker
|
| 46 |
+
registerValidSW(swUrl, config);
|
| 47 |
+
}
|
| 48 |
+
});
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function registerValidSW(swUrl, config) {
|
| 53 |
+
navigator.serviceWorker
|
| 54 |
+
.register(swUrl)
|
| 55 |
+
.then((registration) => {
|
| 56 |
+
registration.onupdatefound = () => {
|
| 57 |
+
const installingWorker = registration.installing;
|
| 58 |
+
if (installingWorker == null) {
|
| 59 |
+
return;
|
| 60 |
+
}
|
| 61 |
+
installingWorker.onstatechange = () => {
|
| 62 |
+
if (installingWorker.state === 'installed') {
|
| 63 |
+
if (navigator.serviceWorker.controller) {
|
| 64 |
+
// At this point, the updated precached content has been fetched,
|
| 65 |
+
// but the previous service worker will still serve the older
|
| 66 |
+
// content until all client tabs are closed.
|
| 67 |
+
console.log(
|
| 68 |
+
'New content is available and will be used when all ' +
|
| 69 |
+
'tabs for this page are closed. See https://cra.link/PWA.'
|
| 70 |
+
);
|
| 71 |
+
|
| 72 |
+
// Execute callback
|
| 73 |
+
if (config && config.onUpdate) {
|
| 74 |
+
config.onUpdate(registration);
|
| 75 |
+
}
|
| 76 |
+
} else {
|
| 77 |
+
// At this point, everything has been precached.
|
| 78 |
+
// It's the perfect time to display a
|
| 79 |
+
// "Content is cached for offline use." message.
|
| 80 |
+
console.log('Content is cached for offline use.');
|
| 81 |
+
|
| 82 |
+
// Execute callback
|
| 83 |
+
if (config && config.onSuccess) {
|
| 84 |
+
config.onSuccess(registration);
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
};
|
| 89 |
+
};
|
| 90 |
+
})
|
| 91 |
+
.catch((error) => {
|
| 92 |
+
console.error('Error during service worker registration:', error);
|
| 93 |
+
});
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
function checkValidServiceWorker(swUrl, config) {
|
| 97 |
+
// Check if the service worker can be found. If it can't reload the page.
|
| 98 |
+
fetch(swUrl, {
|
| 99 |
+
headers: { 'Service-Worker': 'script' },
|
| 100 |
+
})
|
| 101 |
+
.then((response) => {
|
| 102 |
+
// Ensure service worker exists, and that we really are getting a JS file.
|
| 103 |
+
const contentType = response.headers.get('content-type');
|
| 104 |
+
if (
|
| 105 |
+
response.status === 404 ||
|
| 106 |
+
(contentType != null && contentType.indexOf('javascript') === -1)
|
| 107 |
+
) {
|
| 108 |
+
// No service worker found. Probably a different app. Reload the page.
|
| 109 |
+
navigator.serviceWorker.ready.then((registration) => {
|
| 110 |
+
registration.unregister().then(() => {
|
| 111 |
+
window.location.reload();
|
| 112 |
+
});
|
| 113 |
+
});
|
| 114 |
+
} else {
|
| 115 |
+
// Service worker found. Proceed as normal.
|
| 116 |
+
registerValidSW(swUrl, config);
|
| 117 |
+
}
|
| 118 |
+
})
|
| 119 |
+
.catch(() => {
|
| 120 |
+
console.log('No internet connection found. App is running in offline mode.');
|
| 121 |
+
});
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
export function unregister() {
|
| 125 |
+
if ('serviceWorker' in navigator) {
|
| 126 |
+
navigator.serviceWorker.ready
|
| 127 |
+
.then((registration) => {
|
| 128 |
+
registration.unregister();
|
| 129 |
+
})
|
| 130 |
+
.catch((error) => {
|
| 131 |
+
console.error(error.message);
|
| 132 |
+
});
|
| 133 |
+
}
|
| 134 |
+
}
|