aztomiq 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/bin/aztomiq.js +336 -0
  4. package/bin/create-aztomiq.js +77 -0
  5. package/package.json +58 -0
  6. package/scripts/analyze-screenshots.js +217 -0
  7. package/scripts/build.js +39 -0
  8. package/scripts/builds/admin.js +17 -0
  9. package/scripts/builds/assets.js +167 -0
  10. package/scripts/builds/cache.js +48 -0
  11. package/scripts/builds/config.js +31 -0
  12. package/scripts/builds/data.js +210 -0
  13. package/scripts/builds/pages.js +288 -0
  14. package/scripts/builds/playground-examples.js +50 -0
  15. package/scripts/builds/templates.js +118 -0
  16. package/scripts/builds/utils.js +37 -0
  17. package/scripts/create-bug-tracker.js +277 -0
  18. package/scripts/deploy.js +135 -0
  19. package/scripts/feedback-generator.js +102 -0
  20. package/scripts/ui-test.js +624 -0
  21. package/scripts/utils/extract-examples.js +44 -0
  22. package/scripts/utils/migrate-icons.js +67 -0
  23. package/src/includes/breadcrumbs.ejs +100 -0
  24. package/src/includes/cloud-tags.ejs +120 -0
  25. package/src/includes/footer.ejs +37 -0
  26. package/src/includes/generator.ejs +226 -0
  27. package/src/includes/head.ejs +73 -0
  28. package/src/includes/header-data-only.ejs +43 -0
  29. package/src/includes/header.ejs +71 -0
  30. package/src/includes/layout.ejs +68 -0
  31. package/src/includes/legacy-banner.ejs +19 -0
  32. package/src/includes/mega-menu.ejs +80 -0
  33. package/src/includes/schema.ejs +20 -0
  34. package/src/templates/manifest.json.ejs +30 -0
  35. package/src/templates/readme-dist.md.ejs +58 -0
  36. package/src/templates/robots.txt.ejs +4 -0
  37. package/src/templates/sitemap.xml.ejs +69 -0
  38. package/src/templates/sw.js.ejs +78 -0
@@ -0,0 +1,71 @@
1
+ <header class="site-header">
2
+ <div class="header-inner container">
3
+ <div class="nav-item logo-wrapper">
4
+ <a href="<%= rootPath %><%= locale %>/" class="logo-link">
5
+ <img src="<%= rootPath %>assets/images/logo.svg" alt="Logo" class="logo-icon" width="28" height="28">
6
+ <span class="logo-text">
7
+ <%= global.site.title || 'AZtomiq' %>
8
+ </span>
9
+ </a>
10
+
11
+ <button class="menu-trigger dropdown-toggle" aria-haspopup="true" aria-expanded="false" title="Menu"
12
+ style="background: none; border: none; padding: 4px; cursor: pointer; display: flex; align-items: center; margin-left: 5px;">
13
+ <i data-lucide="chevron-down" style="width: 16px; height: 16px; color: var(--text-muted); opacity: 0.6;"></i>
14
+ </button>
15
+
16
+ <%- include('mega-menu.ejs') %>
17
+ </div>
18
+
19
+ <div class="header-actions">
20
+ <!-- Desktop Search Box -->
21
+ <div class="header-search-box desktop-only" id="header-search-box">
22
+ <span class="search-box-icon"><i data-lucide="search" style="width: 18px; height: 18px;"></i></span>
23
+ <input type="text" class="header-search-input"
24
+ placeholder="<%= t('nav.search_placeholder') || 'Search tools...' %>" readonly>
25
+ <kbd>Ctrl+K</kbd>
26
+ </div>
27
+
28
+ <a href="<%= rootPath %><%= locale %>/categories/" class="btn-icon"
29
+ title="<%= t('nav.menu_categories') || 'Categories' %>" aria-label="Categories">
30
+ <i data-lucide="grid-3x3"></i>
31
+ </a>
32
+
33
+ <button id="search-btn-mobile" class="btn-icon search-trigger mobile-only"
34
+ title="<%= t('nav.search_title') || 'Search' %>" aria-label="Search">
35
+ <i data-lucide="search"></i>
36
+ </button>
37
+
38
+ <a href="<%= rootPath %><%= locale === 'vi' ? 'en' : 'vi' %>/<%= pageUrl %>" class="lang-switch"
39
+ title="Switch Language" aria-label="<%= locale === 'vi' ? 'English' : 'Tiếng Việt' %>">
40
+ <span aria-hidden="true">
41
+ <%= locale==='vi' ? '🇺🇸' : '🇻🇳' %>
42
+ </span>
43
+ </a>
44
+ <button id="mode-toggle" class="mode-toggle-btn" title="<%= t('nav.mode_advanced_desc') %>"
45
+ aria-label="Toggle User Mode">
46
+ <span class="mode-text">
47
+ <%= t('nav.mode_standard') || 'Standard' %>
48
+ </span>
49
+ <div class="mode-switch">
50
+ <div class="mode-slider">
51
+ <span class="mode-icon"><i data-lucide="zap" style="width: 12px; height: 12px;"></i></span>
52
+ </div>
53
+ </div>
54
+ <span class="mode-text">
55
+ <%= t('nav.mode_advanced') || 'Advanced' %>
56
+ </span>
57
+ </button>
58
+
59
+ <a href="<%= global.site.github %>" target="_blank" rel="noopener" class="btn-icon" title="GitHub Repository"
60
+ aria-label="GitHub">
61
+ <i data-lucide="github"></i>
62
+ </a>
63
+
64
+ <button id="theme-toggle" class="btn-icon" aria-label="Toggle Dark Mode">
65
+ <i data-lucide="moon"></i>
66
+ </button>
67
+ </div>
68
+ </div>
69
+
70
+ <%- include('header-data-only.ejs') %>
71
+ </header>
@@ -0,0 +1,68 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= locale %>" data-theme="light">
3
+
4
+ <head>
5
+ <%- include('head.ejs') %>
6
+ </head>
7
+
8
+ <body>
9
+ <% if (typeof toolConfig !=='undefined' && toolConfig.hideStandardHeader) { %>
10
+ <!-- Standard Header Hidden for this tool -->
11
+ <% } else { %>
12
+ <%- include('header.ejs') %>
13
+ <% } %>
14
+
15
+ <main class="<%= (typeof toolConfig !== 'undefined' && toolConfig.hideStandardHeader) ? '' : 'container' %>">
16
+ <%- include('breadcrumbs.ejs') %>
17
+ <%- body %>
18
+
19
+ <% if (typeof howToUseHtml !=='undefined' && howToUseHtml) { %>
20
+ <section class="card how-to-use-section" style="margin-top: 3rem;">
21
+ <h2 style="margin-bottom: 1rem;">📖 <%= t('nav.how_to_use') || 'How to use' %>
22
+ </h2>
23
+ <div class="markdown-body">
24
+ <%- howToUseHtml %>
25
+ </div>
26
+ </section>
27
+ <% } %>
28
+ </main>
29
+
30
+ <% if (!(typeof toolConfig !=='undefined' && toolConfig.hideStandardHeader)) { %>
31
+ <%- include('footer.ejs') %>
32
+ <% } %>
33
+
34
+ <% if (typeof changelogHtml !=='undefined' && changelogHtml) { %>
35
+ <!-- Changelog Modal -->
36
+ <div id="changelog-modal" class="search-modal">
37
+ <div class="search-overlay" id="close-changelog-overlay"></div>
38
+ <div class="search-container">
39
+ <div class="search-header">
40
+ <span class="search-icon"><i data-lucide="file-clock"
41
+ style="width: 20px; height: 20px;"></i></span>
42
+ <h2 style="flex: 1; margin: 0; font-size: 1.25rem;">
43
+ <%= t('nav.changelog_title') %>
44
+ </h2>
45
+ <button id="close-changelog" class="close-search">&times;</button>
46
+ </div>
47
+ <div class="changelog-content">
48
+ <%- changelogHtml %>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <% } %>
53
+
54
+ <script src="<%= asset('assets/js/global.js') %>"></script>
55
+ <script>
56
+ // Initialize Lucide icons
57
+ if (window.lucide) {
58
+ lucide.createIcons();
59
+ }
60
+ </script>
61
+
62
+ <% if (typeof toolConfig !=='undefined' && toolConfig.hideStandardHeader) { %>
63
+ <!-- In hidden header mode, we still need search logic and scripts from header.ejs -->
64
+ <%- include('header-data-only.ejs') %>
65
+ <% } %>
66
+ </body>
67
+
68
+ </html>
@@ -0,0 +1,19 @@
1
+ <% const masterTools={ 'tax' : 'salary-tax-master' , 'business-tax' : 'salary-tax-master' , 'freelancer-tax'
2
+ : 'salary-tax-master' , 'social-insurance' : 'salary-tax-master' , 'ot-calculator' : 'salary-tax-master'
3
+ , 'date-toolkit' : 'date-time-master' , 'timestamp-converter' : 'date-time-master' }; const
4
+ masterId=masterTools[featureName]; if (masterId) { %>
5
+ <!-- Banner: Show only if User is in Advanced Mode but viewing a Standard tool -->
6
+ <div class="deprecated-notice mode-advanced-only" style="margin-bottom: 2rem;">
7
+ <span class="icon">�</span>
8
+ <div class="notice-content">
9
+ <strong>
10
+ <%= t('nav.mode_advanced') || 'Advanced Mode' %>
11
+ </strong>:
12
+ <%= t('nav.mode_advanced_desc') %>
13
+ <a href="<%= rootPath %><%= locale %>/<%= masterId %>/" class="btn-link"
14
+ style="font-weight: bold; margin-left: 0.5rem; color: var(--primary-color);">
15
+ <%= t('common.try_master') || 'Open Master Tool' %> →
16
+ </a>
17
+ </div>
18
+ </div>
19
+ <% } %>
@@ -0,0 +1,80 @@
1
+ <div class="dropdown-menu mega-menu">
2
+ <div class="mega-sidebar">
3
+ <div class="mega-sidebar-header">
4
+ <a href="<%= rootPath %><%= locale %>/" class="sidebar-logo logo-link">
5
+ <img src="<%= rootPath %>assets/images/logo.svg" alt="Logo" class="logo-icon" width="28" height="28">
6
+ <span class="logo-text">
7
+ <%= global.site.title || 'AZtomiq' %>
8
+ </span>
9
+ </a>
10
+ </div>
11
+ <div class="mega-sidebar-links">
12
+ <a href="<%= rootPath %><%= locale %>/categories/"
13
+ class="mega-side-link <%= pageUrl === 'categories/' ? 'active' : '' %>">
14
+ <span class="icon-wrap"><i data-lucide="folder-kanban"></i></span>
15
+ <span class="link-text">
16
+ <%= t('nav.menu_categories') || 'Categories' %>
17
+ </span>
18
+ </a>
19
+ <a href="<%= rootPath %><%= locale %>/blog/" class="mega-side-link <%= pageUrl === 'blog/' ? 'active' : '' %>">
20
+ <span class="icon-wrap"><i data-lucide="pen-tool"></i></span>
21
+ <span class="link-text">
22
+ <%= t('nav.menu_blog') || 'Dev Stories' %>
23
+ </span>
24
+ </a>
25
+ <a href="<%= rootPath %><%= locale %>/changelog/"
26
+ class="mega-side-link <%= pageUrl === 'changelog/' ? 'active' : '' %>">
27
+ <span class="icon-wrap"><i data-lucide="history"></i></span>
28
+ <span class="link-text">
29
+ <%= t('nav.changelog_title') || 'Changelogs' %>
30
+ </span>
31
+ </a>
32
+ </div>
33
+ </div>
34
+
35
+ <div class="mega-content-wrap">
36
+ <% const catOrder=global.category_order || []; const catConfig=global.categories || {}; const
37
+ colors=['blue', 'green' , 'slate' , 'pink' , 'orange' , 'purple' , 'indigo' , 'red' ]; %>
38
+ <div class="mega-grid">
39
+ <% catOrder.forEach((catId, index)=> {
40
+ const catTools = tools.filter(t => t.category === catId && (t.status === 'active' || t.status === 'legacy' ||
41
+ t.status === 'not-ready'));
42
+ if (catTools.length === 0) return;
43
+ const meta = catConfig[catId] || {};
44
+ const color = colors[index % colors.length];
45
+ %>
46
+ <div class="mega-cat-card accent-<%= color %>">
47
+ <div class="mega-cat-header">
48
+ <span class="mega-cat-icon"><i data-lucide="<%= meta.icon || 'folder' %>"></i></span>
49
+ <span class="mega-cat-title">
50
+ <%= t(meta.translation_key) || meta.key || catId %>
51
+ </span>
52
+ </div>
53
+ <div class="mega-cat-links">
54
+ <% catTools.forEach(tool=> {
55
+ let modeClass = '';
56
+ if (tool.mode === 'standard') modeClass = 'mode-standard-only';
57
+ else if (tool.mode === 'advanced') modeClass = 'mode-advanced-only';
58
+ const isToolActive = pageUrl === tool.link.substring(1);
59
+ %>
60
+ <a href="<%= rootPath %><%= locale %><%= tool.link %>"
61
+ class="mega-tool-link <%= modeClass %> <%= isToolActive ? 'active' : '' %>">
62
+ <span class="tool-dot"></span>
63
+ <span class="tool-label">
64
+ <%= t(tool.translationKey + '.title' ) %>
65
+ </span>
66
+ <% if (tool.status==='not-ready' ) { %>
67
+ <span class="tool-badge beta">BETA</span>
68
+ <% } else if (tool.highlight) { %>
69
+ <span class="tool-badge hot">
70
+ <i data-lucide="sparkles" style="width: 10px; height: 10px;"></i> HOT
71
+ </span>
72
+ <% } %>
73
+ </a>
74
+ <% }); %>
75
+ </div>
76
+ </div>
77
+ <% }); %>
78
+ </div>
79
+ </div>
80
+ </div>
@@ -0,0 +1,20 @@
1
+ <% var baseUrl=global.site.url; var currentUrl=baseUrl + '/' + locale + '/' + (pageUrl || '' ); /* 1. WebSite Schema */
2
+ var websiteSchema={ "@context" : "https://schema.org" , "@type" : "WebSite" , "name" : "AZtomiq" , "url" :
3
+ baseUrl, "description" : t('meta.description'), "inLanguage" : locale==='vi' ? 'vi-VN' : 'en-US' , "author" :
4
+ { "@type" : "Person" , "name" : global.site.author } }; /* 2. Breadcrumb Schema */ var breadcrumbSchema={ "@context"
5
+ : "https://schema.org" , "@type" : "BreadcrumbList" , "itemListElement" : [ { "@type" : "ListItem" , "position" :
6
+ 1, "name" : "Home" , "item" : baseUrl + '/' + locale + '/' } ] }; if (toolConfig && toolConfig.id) {
7
+ breadcrumbSchema.itemListElement.push({ "@type" : "ListItem" , "position" : 2, "name" : t(toolConfig.translationKey
8
+ + '.title' ), "item" : currentUrl }); } /* 3. Application Schema */ var appSchema=null; if (toolConfig &&
9
+ toolConfig.id) { appSchema={ "@context" : "https://schema.org" , "@type" : "WebApplication" , "name" :
10
+ t(toolConfig.translationKey + '.title' ), "description" : t(toolConfig.translationKey + '.desc' ), "url" :
11
+ currentUrl, "applicationCategory" : "UtilityApplication" , "operatingSystem" : "Any" , "browserRequirements"
12
+ : "Requires JavaScript. Works on all modern browsers (Chrome, Firefox, Safari, Edge)." , "softwareVersion" :
13
+ toolConfig.meta ? toolConfig.meta.version : "1.0.0" , "offers" : { "@type" : "Offer" , "price" : "0" , "priceCurrency"
14
+ : "USD" }, "author" : { "@type" : "Person" , "name" : global.site.author }, "inLanguage" : locale==='vi' ? 'vi-VN'
15
+ : 'en-US' }; if (toolConfig && toolConfig.icon) { appSchema.image=baseUrl + '/assets/images/logo.svg' ; } } var
16
+ allSchemas=[websiteSchema, breadcrumbSchema]; if (appSchema) allSchemas.push(appSchema); %>
17
+
18
+ <script type="application/ld+json">
19
+ <%- JSON.stringify(allSchemas, null, 2) %>
20
+ </script>
@@ -0,0 +1,30 @@
1
+ <%- (function() { const manifest={ name: "AZtomiq - Tiện ích cho mọi người" , short_name: "AZtomiq" ,
2
+ description: "Bộ công cụ tiện ích online miễn phí với kiến trúc nguyên tử." , id: "/?source=pwa" ,
3
+ start_url: "/vi/?source=pwa" , display: "standalone" , orientation: "portrait" , background_color: "#0f172a" ,
4
+ theme_color: "#22d3ee" , icons: [ {
5
+ src: "data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E⚛️%3C/text%3E%3C/svg%3E"
6
+ , sizes: "192x192" , type: "image/svg+xml" , purpose: "any" }, {
7
+ src: "data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E⚛️%3C/text%3E%3C/svg%3E"
8
+ , sizes: "512x512" , type: "image/svg+xml" , purpose: "maskable" } ], screenshots: [ {
9
+ src: "data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1280' height='720' viewBox='0 0 1280 720'%3E%3Crect width='100%25' height='100%25' fill='%230f172a' /%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='100' fill='white'%3EAZtomiq%3C/text%3E%3C/svg%3E"
10
+ , sizes: "1280x720" , type: "image/svg+xml" , form_factor: "wide" , label: "AZtomiq Desktop Home" }, {
11
+ src: "data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='720' height='1280' viewBox='0 0 720 1280'%3E%3Crect width='100%25' height='100%25' fill='%230f172a' /%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='80' fill='white'%3EAZtomiq%3C/text%3E%3C/svg%3E"
12
+ , sizes: "720x1280" , type: "image/svg+xml" , form_factor: "narrow" , label: "AZtomiq Mobile Home" } ], shortcuts:
13
+ tools.filter(t=> t.status === 'active').slice(0, 4).map(tool => ({
14
+ name: t(tool.translationKey + '.title'),
15
+ short_name: t(tool.translationKey + '.title'),
16
+ description: t(tool.translationKey + '.desc').replace(/\n/g, ' '),
17
+ url: `/${defaultLocale}${tool.link}?source=pwa`,
18
+ icons: [
19
+ {
20
+ src: `data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext
21
+ y='.9em' font-size='90'%3E${tool.icon}%3C/text%3E%3C/svg%3E`,
22
+ sizes: "192x192",
23
+ type: "image/svg+xml"
24
+ }
25
+ ]
26
+ }))
27
+ };
28
+ return JSON.stringify(manifest, null, 2);
29
+ })()
30
+ %>
@@ -0,0 +1,58 @@
1
+ # AZtomiq - Production Build
2
+
3
+ [Tiếng Việt](#tiếng-viet) | [English](#english)
4
+
5
+ ---
6
+
7
+ <a name="tiếng-viet"></a>
8
+ ## 🇻🇳 Tiếng Việt
9
+
10
+ Đây là kho lưu trữ chứa các tệp tin đã được biên dịch (build) cho trang web
11
+ [AZtomiq.vercel.app](https://aztomiq.vercel.app/).
12
+
13
+ ### 🚀 Về Dự Án
14
+ AZtomiq là một bộ công cụ số toàn diện, ưu tiên quyền riêng tư, được thiết kế để cung cấp các tiện ích chất lượng cao
15
+ cho
16
+ các tác vụ hàng ngày.
17
+
18
+ - **Quyền riêng tư là trên hết**: Tất cả các xử lý được thực hiện cục bộ trong trình duyệt của bạn. Không có dữ liệu nào
19
+ được gửi lên máy chủ.
20
+ - **Nhanh & Nhẹ**: Được xây dựng dưới dạng trang web tĩnh hiệu suất cao.
21
+ - **Truy cập tự do**: Miễn phí sử dụng, không có quảng cáo xâm lấn hoặc theo dõi.
22
+
23
+ ### 🛠 Tính năng trong bản Build này
24
+ - **Hỗ trợ đa ngôn ngữ**: Có sẵn tiếng Việt và tiếng Anh.
25
+ - **Sẵn sàng cho PWA**: Có thể cài đặt trên điện thoại và máy tính như một ứng dụng gốc.
26
+ - **Tối ưu hóa**: CSS, JS được nén và hình ảnh được tối ưu để tải trang nhanh như chớp.
27
+
28
+ ### 🔗 Liên kết
29
+ - **Trang chủ**: [https://aztomiq.vercel.app/](https://aztomiq.vercel.app/)
30
+ - **Tác giả**: [Anph](https://github.com/ph4n4n)
31
+
32
+ ---
33
+
34
+ <a name="english"></a>
35
+ ## 🇺🇸 English
36
+
37
+ This repository contains the production-ready assets for [AZtomiq.vercel.app](https://aztomiq.vercel.app/).
38
+
39
+ ### 🚀 About the Project
40
+ AZtomiq is a comprehensive, privacy-first digital toolbox designed to provide high-quality utility tools for everyday
41
+ tasks.
42
+
43
+ - **Privacy-First**: All processing is done locally in your browser. Nothing is sent to our servers.
44
+ - **Fast & Lightweight**: Built as a high-performance static site.
45
+ - **Open Access**: Free to use without invasive tracking or ads.
46
+
47
+ ### 🛠 Features in this Build
48
+ - **Multilingual Support**: Available in Vietnamese and English.
49
+ - **PWA Ready**: Can be installed on mobile and desktop as a native app.
50
+ - **Optimized Assets**: Minified CSS, JS, and compressed images for lightning-fast load times.
51
+
52
+ ### 🔗 Links
53
+ - **Main Site**: [https://aztomiq.vercel.app/](https://aztomiq.vercel.app/)
54
+ - **Author**: [Anph](https://github.com/ph4n4n)
55
+
56
+ ---
57
+ *Lưu ý: Kho lưu trữ này được cập nhật tự động. Để xem mã nguồn hoặc đóng góp, vui lòng liên hệ với tác giả.*
58
+ *Note: This repository is automatically updated. For source code or contributions, please contact the author.*
@@ -0,0 +1,4 @@
1
+ User-agent: *
2
+ Allow: /
3
+ Disallow: /assets/
4
+ Sitemap: <%= global.site.url %>/sitemap.xml
@@ -0,0 +1,69 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
3
+ <% var baseUrl=global.site.url; var allPages=[]; // 1. Root and Static Pages for each locale var
4
+ staticPages=['', 'about/' , 'categories/' , 'changelog/' , 'privacy/' , 'terms/' ]; locales.forEach(loc=> {
5
+ staticPages.forEach(page => {
6
+ var alts = locales.map(l => ({
7
+ hreflang: l === 'vi' ? 'vi-VN' : 'en-US',
8
+ href: baseUrl + '/' + l + '/' + page
9
+ }));
10
+
11
+ allPages.push({
12
+ loc: baseUrl + '/' + loc + '/' + page,
13
+ priority: page === '' ? '1.0' : '0.6',
14
+ changefreq: 'weekly',
15
+ alternates: alts
16
+ });
17
+ });
18
+ });
19
+
20
+ // 2. Blog (Feature page)
21
+ locales.forEach(loc => {
22
+ var alts = locales.map(l => ({
23
+ hreflang: l === 'vi' ? 'vi-VN' : 'en-US',
24
+ href: baseUrl + '/' + l + '/blog/'
25
+ }));
26
+ allPages.push({
27
+ loc: baseUrl + '/' + loc + '/blog/',
28
+ priority: '0.7',
29
+ changefreq: 'weekly',
30
+ alternates: alts
31
+ });
32
+ });
33
+
34
+ // 3. Active Tools
35
+ tools.forEach(tool => {
36
+ if (tool.status === 'active') {
37
+ locales.forEach(loc => {
38
+ var alts = locales.map(l => ({
39
+ hreflang: l === 'vi' ? 'vi-VN' : 'en-US',
40
+ href: baseUrl + '/' + l + tool.link
41
+ }));
42
+
43
+ allPages.push({
44
+ loc: baseUrl + '/' + loc + tool.link,
45
+ priority: '0.8',
46
+ changefreq: 'monthly',
47
+ alternates: alts
48
+ });
49
+ });
50
+ }
51
+ });
52
+ %>
53
+ <% for (var i=0; i < allPages.length; i++) { var page=allPages[i]; %>
54
+ <url>
55
+ <loc>
56
+ <%= page.loc %>
57
+ </loc>
58
+ <changefreq>
59
+ <%= page.changefreq %>
60
+ </changefreq>
61
+ <priority>
62
+ <%= page.priority %>
63
+ </priority>
64
+ <% for (var j=0; j < page.alternates.length; j++) { var alt=page.alternates[j]; %>
65
+ <xhtml:link rel="alternate" hreflang="<%= alt.hreflang %>" href="<%= alt.href %>" />
66
+ <% } %>
67
+ </url>
68
+ <% } %>
69
+ </urlset>
@@ -0,0 +1,78 @@
1
+ const CACHE_NAME = 'aztomiq-v1';
2
+ const IS_DEV = <%= isDev %>;
3
+ const STATIC_ASSETS = [
4
+ './',
5
+ './manifest.json',
6
+ './assets/css/global.css',
7
+ './assets/js/global.js',
8
+ <% const assets=new Set(); locales.forEach(lang=> {
9
+ assets.add(`./${lang}/`);
10
+ assets.add(`./${lang}/index.html`);
11
+ });
12
+
13
+ tools.forEach(tool => {
14
+ assets.add(`./assets/features/${tool.id}/style.css`);
15
+ assets.add(`./assets/features/${tool.id}/script.js`);
16
+ locales.forEach(lang => {
17
+ assets.add(`./${lang}/${tool.id}/`);
18
+ });
19
+ });
20
+
21
+ const assetList = Array.from(assets);
22
+ assetList.forEach((a, i) => { -%>
23
+ '<%= a %>'<%= i < assetList.length - 1 ? ',' : '' %>
24
+ <% }); -%>
25
+ ];
26
+
27
+ // Install Event
28
+ self.addEventListener('install', (e) => {
29
+ if (IS_DEV) {
30
+ self.skipWaiting();
31
+ return;
32
+ }
33
+ console.log('[SW] Installing...');
34
+ e.waitUntil(
35
+ caches.open(CACHE_NAME).then((cache) => {
36
+ console.log('[SW] Caching App Shell');
37
+ return cache.addAll(STATIC_ASSETS);
38
+ })
39
+ );
40
+ self.skipWaiting();
41
+ });
42
+
43
+ // Activate Event
44
+ self.addEventListener('activate', (e) => {
45
+ console.log('[SW] Activating...');
46
+ e.waitUntil(
47
+ caches.keys().then((keys) =>
48
+ Promise.all(
49
+ keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
50
+ )
51
+ )
52
+ );
53
+ self.clients.claim();
54
+ });
55
+
56
+ // Fetch Event
57
+ self.addEventListener('fetch', (e) => {
58
+ if (IS_DEV) return; // Skip cache in dev mode
59
+
60
+ if (e.request.method !== 'GET' || !e.request.url.startsWith(self.location.origin)) {
61
+ return;
62
+ }
63
+
64
+ e.respondWith(
65
+ caches.open(CACHE_NAME).then(async (cache) => {
66
+ const cachedResponse = await cache.match(e.request);
67
+ const fetchPromise = fetch(e.request).then((networkResponse) => {
68
+ if (networkResponse.ok) {
69
+ cache.put(e.request, networkResponse.clone());
70
+ }
71
+ return networkResponse;
72
+ }).catch((err) => {
73
+ console.warn('[SW] Network fail:', err);
74
+ });
75
+ return cachedResponse || fetchPromise;
76
+ })
77
+ );
78
+ });