Skip to content

Commit

Permalink
Merge branch 'master' into i18n
Browse files Browse the repository at this point in the history
Signed-off-by: Carl <Carlgo11@users.noreply.github.com>
  • Loading branch information
Carlgo11 authored Nov 21, 2024
2 parents b4605e7 + 95425dc commit 91a95f7
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 20 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Build Website
on:
pull_request:
branches:
- master

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Install dependencies
run: npm ci

- name: Build Site
run: npm run build

- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ github.event.number }}
path: dist
64 changes: 64 additions & 0 deletions functions/sitemap.xml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const api_url = 'https://api.2fa.directory/frontend/v1/';
const base = 'https://2fa.directory';
let urls = [
`${base}/privacy`,
`${base}/companies`,
`${base}/bots`,
`${base}/api`];
const init = {
cf: {
cacheEverything: true,
cacheTtl: 60 * 60 * 24 * 30, // Cache request 1 month
},
};
const headers = {
'Content-Type': 'application/xml',
'Cache-Control': 'public, max-age=2592000, immutable', // 1 month cache, no revalidation
};

export async function onRequestGet({request, waitUntil}) {
const cacheKey = new Request(request.url, request);
const cache = caches.default;
let response = await cache.match(cacheKey);

// If response is cached, return it
if (response)
return response;

// If no response is found, continue
const regions_fetch = await fetch(`${api_url}/regions.json`, init);
const regions = await regions_fetch.json();

// Fetch listed categories for each region
for (const region of Object.keys(regions)) {
const categories_fetch = await fetch(
`${api_url}/${region}/categories.json`, init);
const categories = Object.keys(await categories_fetch.json());
categories.forEach(category => {
urls.push(`${base}/${region}/#${category}`);
});
}

// Create XML document
let output = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'];

// Add each URL to the URL set
urls.forEach((url) => {
output.push('<url>');
output.push(`<loc>${url}</loc>`);
output.push('<changefreq>monthly</changefreq>');
output.push('</url>');
});

// Close URL set
output.push('</urlset>');

response = new Response(output.join(''), {
headers,
});

waitUntil(cache.put(cacheKey, response.clone()));
return response;
}
Binary file modified public/icons/maskable.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 17 additions & 8 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
{
"name": "2FA Directory",
"lang": "en",
"start_url": "/",
"scope": "/",
"serviceworker": {
"src": "/service-worker.js",
"scope": "/",
"use_cache": true
},
"theme_color": "#38CCCC",
"background_color": "#EEE",
"display": "standalone",
"display_override": [
"standalone",
"minimal-ui"
],
"description": "List of sites with two factor auth support which includes SMS, email, phone calls, hardware, and software.",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-only.svg",
"src": "/icons/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon.png",
"sizes": "515x515",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
Expand All @@ -22,10 +36,5 @@
"type": "image/png",
"purpose": "maskable"
}
],
"start_url": "/",
"scope": "/",
"theme_color": "#38CCCC",
"background_color": "#EEE",
"display": "standalone"
]
}
1 change: 1 addition & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://2fa.directory/sitemap.xml
63 changes: 63 additions & 0 deletions public/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const DYNAMIC_CACHE = "dynamic-cache-v1";

// Array of regular expressions for URLs to match for dynamic caching
const URLS_TO_CACHE = [
/^https:\/\/fonts\.googleapis\.com/,
/^https:\/\/fonts\.gstatic\.com/,
/^https:\/\/2fa\.directory\/assets\/.*/, // Match specific assets on 2fa.directory
/^https:\/\/api\.2fa\.directory\/frontend\/v1\/regions\.json$/, // Match specific JSON files
/^https:\/\/api\.2fa\.directory\/frontend\/v1\/[a-z]{2}\/categories\.json$/, // Match only specific JSON paths by region
/^https:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/.*/, // Match CDN assets from Cloudflare
];

// Function to check if the URL matches any in the array using regular expressions
function shouldCache(url) {
return URLS_TO_CACHE.some((regex) => regex.test(url));
}

// Install event: skipWaiting allows for more aggressive unloading
self.addEventListener("install", (event) => {
self.skipWaiting();
});

// Activate event: Clean up old caches
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.filter((cacheName) => cacheName !== DYNAMIC_CACHE).
map((cacheName) => caches.delete(cacheName)),
);
}),
);
self.clients.claim();
});

// Fetch event: Serve cached assets if available; otherwise, fetch from network and cache the response
self.addEventListener("fetch", (event) => {
const requestUrl = event.request.url;

// Check if the request URL should be cached dynamically
if (shouldCache(requestUrl)) {
// Network-first strategy for URLs in the matching array
event.respondWith(
fetch(event.request).then((response) => {
// Clone and store the response in the dynamic cache
return caches.open(DYNAMIC_CACHE).then((cache) => {
cache.put(event.request, response.clone());
return response;
});
}).catch(() => caches.match(event.request)), // Fallback to cache on network failure
);
} else {
// Cache-first strategy for other requests (but do not cache if not matching)
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) return cachedResponse;

// Fetch from network without caching the result
return fetch(event.request);
}),
);
}
});
3 changes: 3 additions & 0 deletions src/components/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ import "/assets/css/root.scss";
import "/assets/css/page.scss";
import "/assets/css/navbar.scss";
import "/assets/css/footer.scss";
import {registerServiceWorker} from '../serviceWorker.js';

registerServiceWorker();
22 changes: 10 additions & 12 deletions src/components/table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ function Head({category}) {
}

function Methods({methods, customSoftware, customHardware}) {
const mfaPopoverConfig = {
html: true,
sanitize: false,
trigger: "hover focus"
};

useEffect(() => {
[...document.querySelectorAll('.note')].map((el) => new Popover(el, {
trigger: 'hover focus',
Expand Down Expand Up @@ -216,18 +222,10 @@ function Contact({contact}) {
const lang = contact.language || 'en';
return (
<div aria-label="2FA not supported" className="contact">
{contact.twitter && (<button className="contact-btn twitter"
onClick={() => socialMediaNotice('tweet',
lang, contact.twitter)}></button>)}
{contact.facebook && (<button className="contact-btn facebook"
onClick={() => socialMediaNotice('facebook',
lang, contact.twitter)}></button>)}
{contact.email && (<button className="contact-btn email"
onClick={() => socialMediaNotice('email', lang,
contact.twitter)}></button>)}
{contact.form && (<button className="contact-btn form"
onClick={() => window.open(contact.form,
'_blank')}></button>)}
{contact.twitter && (<button className="contact-btn twitter" onClick={() => socialMediaNotice("tweet", lang, contact.twitter)}></button>)}
{contact.facebook && (<button className="contact-btn facebook" onClick={() => socialMediaNotice("facebook", lang, contact.facebook)}></button>)}
{contact.email && (<button className="contact-btn email" onClick={() => socialMediaNotice("email", lang, contact.email)}></button>)}
{contact.form && (<button className="contact-btn form" onClick={() => window.open(contact.form, "_blank")}></button>)}
</div>
);
}
Expand Down
23 changes: 23 additions & 0 deletions src/serviceWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then((registration) => {
console.log('Service Worker registered with scope:', registration.scope);

// Listen for updates to the service worker
registration.onupdatefound = () => {
const newWorker = registration.installing;
if (newWorker) {
newWorker.onstatechange = () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New content is available, notify the user or auto-refresh
console.log('New Service Worker found. Refresh for updates.');
}
};
}
};
})
.catch((error) => console.error('Service Worker registration failed:', error));
}
}

0 comments on commit 91a95f7

Please sign in to comment.