4sp-dv 1.1.5 → 1.1.7
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.
- package/4simpleproblems_v5.html +54 -2
- package/4sp-combination-test.html +739 -0
- package/logged-in/dashboard.html +89 -1
- package/package.json +1 -1
package/4simpleproblems_v5.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>4SP - VERSION 5 CLIENT</title>
|
|
7
7
|
<link rel="icon" type="image/png" href="https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo.png">
|
|
8
8
|
|
|
9
|
-
<base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.1.
|
|
9
|
+
<base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.1.6/logged-in/">
|
|
10
10
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
11
11
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
|
12
12
|
|
|
@@ -851,7 +851,7 @@
|
|
|
851
851
|
const displayUsername = document.getElementById('display-username');
|
|
852
852
|
const displayEmail = document.getElementById('display-email');
|
|
853
853
|
|
|
854
|
-
const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.1.
|
|
854
|
+
const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.1.6/logged-in/';
|
|
855
855
|
|
|
856
856
|
// Preload Logos
|
|
857
857
|
const preloadImgs = [
|
|
@@ -1580,6 +1580,58 @@
|
|
|
1580
1580
|
// Normal fetch flow for ALL pages
|
|
1581
1581
|
const url = BASE_URL + pageName;
|
|
1582
1582
|
|
|
1583
|
+
// SPECIAL HANDLING FOR GAMES.HTML: Isolated shell to fix file pulling
|
|
1584
|
+
if (pageName.includes('games.html')) {
|
|
1585
|
+
fetch(url)
|
|
1586
|
+
.then(res => res.text())
|
|
1587
|
+
.then(html => {
|
|
1588
|
+
const oldFrame = document.getElementById('app-frame');
|
|
1589
|
+
const newFrame = document.createElement('iframe');
|
|
1590
|
+
newFrame.id = 'app-frame';
|
|
1591
|
+
newFrame.style.cssText = "width: 100%; height: 100%; border: none; display: block; flex-grow: 1;";
|
|
1592
|
+
if (oldFrame) {
|
|
1593
|
+
newFrame.style.cssText = oldFrame.style.cssText;
|
|
1594
|
+
if (oldFrame.parentNode) oldFrame.parentNode.replaceChild(newFrame, oldFrame);
|
|
1595
|
+
} else {
|
|
1596
|
+
document.body.appendChild(newFrame);
|
|
1597
|
+
}
|
|
1598
|
+
appFrame = newFrame;
|
|
1599
|
+
|
|
1600
|
+
const doc = newFrame.contentWindow.document;
|
|
1601
|
+
doc.open();
|
|
1602
|
+
// Minimal injection to fix paths and preserve parent communication
|
|
1603
|
+
const baseTag = '<base href="' + BASE_URL + '">';
|
|
1604
|
+
const patch = `
|
|
1605
|
+
<script>
|
|
1606
|
+
window.openSettings = function() { window.parent.openSettings(); };
|
|
1607
|
+
document.addEventListener('click', e => {
|
|
1608
|
+
const link = e.target.closest('a');
|
|
1609
|
+
if (link && link.href && (link.href.includes('settings.html') || link.href === '#settings')) {
|
|
1610
|
+
e.preventDefault();
|
|
1611
|
+
window.parent.openSettings();
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
<\/script>
|
|
1615
|
+
`;
|
|
1616
|
+
let modifiedHtml = html.replace('<head>', '<head>' + baseTag + patch);
|
|
1617
|
+
doc.write(modifiedHtml);
|
|
1618
|
+
doc.close();
|
|
1619
|
+
|
|
1620
|
+
if (loaderBar) loaderBar.style.width = "100%";
|
|
1621
|
+
setTimeout(() => {
|
|
1622
|
+
if (loader) {
|
|
1623
|
+
loader.classList.add('opacity-0');
|
|
1624
|
+
loader.classList.remove('active');
|
|
1625
|
+
setTimeout(() => {
|
|
1626
|
+
loader.classList.add('hidden');
|
|
1627
|
+
if(loaderBar) loaderBar.style.width = "0%";
|
|
1628
|
+
}, 200);
|
|
1629
|
+
}
|
|
1630
|
+
}, 200);
|
|
1631
|
+
});
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1583
1635
|
fetch(url)
|
|
1584
1636
|
.then(res => res.text())
|
|
1585
1637
|
.then(html => {
|
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>4SP - GAMES CLIENT</title>
|
|
7
|
+
<link rel="icon" type="image/png" href="https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo.png">
|
|
8
|
+
|
|
9
|
+
<base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.1.6/logged-in/">
|
|
10
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
|
12
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
|
13
|
+
|
|
14
|
+
<script src="https://cdn.jsdelivr.net/npm/fireworks-js@2.x/dist/index.umd.js"></script>
|
|
15
|
+
<!-- Analytics -->
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/4sp-dv@latest/analytics.js"></script>
|
|
17
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-1D4F692C1Q"></script>
|
|
18
|
+
<script>
|
|
19
|
+
window.dataLayer = window.dataLayer || [];
|
|
20
|
+
function gtag(){dataLayer.push(arguments);}
|
|
21
|
+
gtag('js', new Date());
|
|
22
|
+
gtag('config', 'G-1D4F692C1Q');
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
/* Merged Styles from 4simpleproblems_v5.html */
|
|
27
|
+
body {
|
|
28
|
+
background-color: #040404;
|
|
29
|
+
color: #c0c0c0;
|
|
30
|
+
font-family: 'Geist', sans-serif;
|
|
31
|
+
height: 100vh;
|
|
32
|
+
margin: 0;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
font-weight: 300;
|
|
36
|
+
overflow: hidden; /* Prevent body scroll, main content scrolls */
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
h1, h2, h3, .font-bold, .font-semibold, strong, b, .tracking-widest {
|
|
40
|
+
font-weight: 400 !important;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Navbar Styles */
|
|
44
|
+
#navbar-container {
|
|
45
|
+
background: var(--navbar-bg, #000000);
|
|
46
|
+
border-bottom: 1px solid var(--navbar-border, #1f2937);
|
|
47
|
+
height: 64px;
|
|
48
|
+
width: 100%;
|
|
49
|
+
display: none; /* Hidden until unlocked */
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
padding: 0 1rem;
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
z-index: 60;
|
|
56
|
+
position: relative;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#fireworks-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; opacity: 0; transition: opacity 0.5s ease; }
|
|
60
|
+
#navbar-container > *:not(#fireworks-container) { position: relative; z-index: 10; }
|
|
61
|
+
.navbar-logo { height: 40px; width: auto; transition: filter 0.3s ease; }
|
|
62
|
+
|
|
63
|
+
.tab-wrapper { flex-grow: 1; display: flex; align-items: center; position: relative; min-width: 0; margin: 0 1rem; justify-content: center; overflow: hidden; }
|
|
64
|
+
.tab-scroll-container { display: flex; align-items: center; gap: 0.5rem; overflow-x: auto; scrollbar-width: none; white-space: nowrap; max-width: 100%; scroll-behavior: smooth; padding-left: 20px; padding-right: 20px; }
|
|
65
|
+
.tab-scroll-container::-webkit-scrollbar { display: none; }
|
|
66
|
+
.scroll-glide-button { position: absolute; top: 0; height: 100%; width: 60px; display: flex; align-items: center; justify-content: center; color: var(--glide-btn-color, #ffffff); font-size: 1rem; cursor: pointer; opacity: 1; transition: opacity 0.3s; z-index: 55; background: transparent; border: none; }
|
|
67
|
+
#glide-left { left: 0; background-color: var(--navbar-bg, #000000); -webkit-mask-image: linear-gradient(to right, black 30%, transparent); mask-image: linear-gradient(to right, black 30%, transparent); justify-content: flex-start; padding-left: 8px; }
|
|
68
|
+
#glide-right { right: 0; background-color: var(--navbar-bg, #000000); -webkit-mask-image: linear-gradient(to left, black 30%, transparent); mask-image: linear-gradient(to left, black 30%, transparent); justify-content: flex-end; padding-right: 8px; }
|
|
69
|
+
.scroll-glide-button.hidden { opacity: 0 !important; pointer-events: none !important; }
|
|
70
|
+
|
|
71
|
+
.nav-tab { padding: 0.5rem 1rem; color: var(--tab-text, #9ca3af); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; transition: all 0.2s; cursor: pointer; flex-shrink: 0; }
|
|
72
|
+
.nav-tab:hover { color: var(--tab-hover-text, #ffffff); background-color: var(--tab-hover-bg, rgba(79, 70, 229, 0.05)); border-color: var(--tab-active-border, #4f46e5); transform: translateY(-1px); z-index: 50; }
|
|
73
|
+
.nav-tab.active { color: var(--tab-active-text, #4f46e5); border-color: var(--tab-active-border, #4f46e5); background-color: var(--tab-active-bg, rgba(79, 70, 229, 0.1)); }
|
|
74
|
+
|
|
75
|
+
.auth-controls-wrapper { display: flex; align-items: center; gap: 1rem; position: relative; }
|
|
76
|
+
.icon-btn { width: 40px; height: 40px; border-radius: 50%; border: 1px solid #4b5563; display: flex; align-items: center; justify-content: center; color: #d1d5db; cursor: pointer; background: transparent; transition: background 0.2s; position: relative; }
|
|
77
|
+
.icon-btn:hover { background-color: #374151; color: white; z-index: 50; }
|
|
78
|
+
#auth-btn, #auth-btn:hover, #auth-btn:active { transform: none !important; transition: none !important; background-color: transparent !important; z-index: 65 !important; cursor: pointer !important; padding: 0; overflow: hidden; border: 1px solid #4b5563; }
|
|
79
|
+
|
|
80
|
+
.auth-menu { position: absolute; right: 0; top: 55px; width: 16rem; background: var(--menu-bg, #000); border: 1px solid var(--menu-border, #333); border-radius: 1.5rem; padding: 0.75rem; display: none; flex-direction: column; gap: 0.5rem; z-index: 70; box-shadow: 0 10px 30px rgba(0,0,0,0.6); transform-origin: top right; }
|
|
81
|
+
.auth-menu.open { display: flex; animation: menu-pop-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; }
|
|
82
|
+
@keyframes menu-pop-in { 0% { opacity: 0; transform: translateY(-10px) scale(0.95); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
|
|
83
|
+
.auth-menu-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; color: var(--menu-item-text, #d1d5db); background: var(--menu-item-bg, #0a0a0a); border: 1px solid var(--menu-item-border, #333); border-radius: 1rem; text-decoration: none; font-size: 0.9rem; font-weight: 500; transition: all 0.3s; cursor: pointer; }
|
|
84
|
+
.auth-menu-item:hover { background-color: var(--menu-item-hover-bg, #000); border-color: var(--menu-item-hover-border, #fff); color: var(--menu-item-hover-text, #fff); transform: translateY(-2px) scale(1.02); }
|
|
85
|
+
.auth-header { padding: 0.5rem 1rem; border-bottom: 1px solid var(--menu-divider, #333); margin-bottom: 0.25rem; }
|
|
86
|
+
.auth-username { color: var(--menu-username-text, white); font-weight: 400; font-size: 0.9rem; }
|
|
87
|
+
.auth-email { color: var(--menu-email-text, #9ca3af); font-size: 0.75rem; }
|
|
88
|
+
|
|
89
|
+
/* Merged Styles from games.html */
|
|
90
|
+
.games-main-container {
|
|
91
|
+
flex-grow: 1;
|
|
92
|
+
overflow-y: auto;
|
|
93
|
+
padding: 1.5rem;
|
|
94
|
+
padding-bottom: 100px;
|
|
95
|
+
background-color: #040404;
|
|
96
|
+
position: relative;
|
|
97
|
+
display: none; /* Hidden until loaded/authenticated */
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.btn-toolbar-style { background: #000; border: 1px solid #333; border-radius: 14px; color: #d1d5db; padding: 0.5rem 1rem; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 0.5rem; }
|
|
101
|
+
.btn-card-action { display: inline-flex; align-items: center; justify-content: center; width: 2.25rem; height: 2.25rem; border-radius: 14px; background: transparent; border: 1px solid transparent; color: #9ca3af; cursor: pointer; transition: all 0.2s; font-size: 1rem; }
|
|
102
|
+
.btn-card-action:hover { background-color: rgba(79, 70, 229, 0.1); color: #4f46e5; border-color: #4f46e5; transform: scale(1.1); }
|
|
103
|
+
|
|
104
|
+
.zone-item { transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; }
|
|
105
|
+
.zone-item:hover { transform: translateY(-4px); box-shadow: 0 8px 25px rgba(79, 70, 229, 0.2); }
|
|
106
|
+
.other-zone-item { min-height: 250px; position: relative; z-index: 1; }
|
|
107
|
+
.other-zone-item:hover { box-shadow: 0 8px 25px rgba(170, 170, 170, 0.2); z-index: 10; }
|
|
108
|
+
.other-zone-item .image-overlay { background: linear-gradient(90deg, rgba(7,7,7,0.85) 0%, rgba(7,7,7,0) 40%, rgba(7,7,7,0) 60%, rgba(7,7,7,0.85) 100%); border-radius: 1.25rem; }
|
|
109
|
+
|
|
110
|
+
#bottom-fixed-bar { transition: transform 0.4s, opacity 0.3s; }
|
|
111
|
+
#bottom-fixed-bar:focus-within { transform: translateY(-5px); border-color: #4f46e5; box-shadow: 0 10px 30px rgba(0,0,0,0.8); }
|
|
112
|
+
.search-results-container { max-height: 300px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #333 transparent; }
|
|
113
|
+
.search-item { display: flex; cursor: pointer; color: #e5e7eb; padding: 10px 15px; }
|
|
114
|
+
.search-item:hover { background-color: rgba(255, 255, 255, 0.05); padding-left: 20px; }
|
|
115
|
+
.category-badge { font-size: 0.6rem; padding: 1px 6px; border-radius: 6px; border: 1px solid currentColor; display: inline-block; }
|
|
116
|
+
|
|
117
|
+
#zoneViewer { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: #000; z-index: 5000; flex-direction: column; opacity: 0; }
|
|
118
|
+
#zoneViewer.active { display: flex; opacity: 1; }
|
|
119
|
+
#zoneViewer .zone-header { background-color: #0a0a0a; padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #1a1a1a; box-shadow: 0 2px 10px rgba(0,0,0,0.5); }
|
|
120
|
+
#zoneViewer .zone-header .zone-title h2 { margin: 0; font-size: 1.2rem; color: #fff; font-weight: 500; }
|
|
121
|
+
#zoneViewer .zone-header .zone-controls { display: flex; gap: 8px; }
|
|
122
|
+
#zoneViewer iframe { flex-grow: 1; border: none; background-color: #000; }
|
|
123
|
+
|
|
124
|
+
.eagler-dropdown { position: absolute; bottom: 120%; right: 0; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255,255,255,0.15); border-radius: 0.75rem; padding: 0.5rem; z-index: 1000; min-width: 200px; display: none; }
|
|
125
|
+
.eagler-dropdown.show { display: block; }
|
|
126
|
+
.eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; border-radius: 0.5rem; text-decoration: none; }
|
|
127
|
+
.eagler-dropdown-link:hover { background-color: rgba(255,255,255,0.1); color: white; }
|
|
128
|
+
|
|
129
|
+
#creditsModal { display: none; position: fixed; top: 0; left: 0; w: 100%; h: 100%; bg: black/70; backdrop-filter: blur(10px); z-index: 6000; justify-content: center; align-items: center; }
|
|
130
|
+
#instruction-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 8000; display: none; align-items: center; justify-content: center; }
|
|
131
|
+
|
|
132
|
+
/* Lock Screen */
|
|
133
|
+
#lock-screen { position: fixed; inset: 0; background: #040404; z-index: 9999; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
|
134
|
+
</style>
|
|
135
|
+
</head>
|
|
136
|
+
<body>
|
|
137
|
+
|
|
138
|
+
<!-- LOCK SCREEN -->
|
|
139
|
+
<div id="lock-screen">
|
|
140
|
+
<div class="max-w-5xl w-full bg-[#040404] flex flex-col md:flex-row border border-[#252525] rounded-3xl overflow-hidden shadow-2xl p-4">
|
|
141
|
+
<div class="flex-1 p-8 flex flex-col justify-center items-center border-b md:border-b-0 md:border-r border-[#252525]">
|
|
142
|
+
<h2 class="text-3xl text-[#c0c0c0] mb-6 text-center w-full font-light tracking-tighter">4SP Version 5 Client (DV)</h2>
|
|
143
|
+
<p class="text-[#505050] text-center mb-8 text-sm">Enter your 12-character access code.</p>
|
|
144
|
+
<input type="text" id="codeInput" class="w-full max-w-xs bg-[#111] border border-[#252525] text-white rounded-xl p-3 text-center tracking-[0.2em] mb-4 outline-none focus:border-indigo-500 transition" placeholder="XXXX-XXXX-XXXX">
|
|
145
|
+
<button id="unlockBtn" class="w-full max-w-xs py-2 px-4 text-sm font-medium rounded-xl text-indigo-500 bg-indigo-500/10 border border-indigo-500 hover:bg-indigo-500/20 transition mb-4">Access</button>
|
|
146
|
+
<p id="lockMessage" class="text-red-400 text-xs mt-2 min-h-[20px] text-center"></p>
|
|
147
|
+
<p id="loading-text" class="text-gray-500 text-xs mt-2 hidden text-center">Verifying...</p>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="flex-1 p-8 flex flex-col justify-center items-center bg-[#040404]">
|
|
150
|
+
<h2 class="text-3xl text-[#c0c0c0] mb-6 text-center w-full font-light tracking-tighter">How it works</h2>
|
|
151
|
+
<p class="text-lg text-[#505050] mb-8 text-center max-w-md font-light leading-relaxed">This client provides secure, local access to the 4SP suite. Generate a unique code from your web dashboard to log in properly.</p>
|
|
152
|
+
<a href="https://4sp-organization.github.io/connection.html" target="_blank" class="w-full max-w-xs py-2 px-4 text-sm font-medium rounded-xl text-cyan-500 bg-cyan-500/10 border border-cyan-500 hover:bg-cyan-500/20 transition text-center flex items-center justify-center">Get Your Code</a>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<!-- NAVBAR -->
|
|
158
|
+
<div id="navbar-container">
|
|
159
|
+
<div id="fireworks-container"></div>
|
|
160
|
+
<img src="https://cdn.jsdelivr.net/npm/4sp-asset-library@latest/logo.png" id="navbar-logo" class="navbar-logo" alt="Logo">
|
|
161
|
+
<div class="tab-wrapper">
|
|
162
|
+
<button id="glide-left" class="scroll-glide-button hidden"><i class="fa-solid fa-chevron-left"></i></button>
|
|
163
|
+
<div class="tab-scroll-container" id="tabs-container">
|
|
164
|
+
<!-- Tabs injected here -->
|
|
165
|
+
</div>
|
|
166
|
+
<button id="glide-right" class="scroll-glide-button hidden"><i class="fa-solid fa-chevron-right"></i></button>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="auth-controls-wrapper">
|
|
169
|
+
<div class="relative">
|
|
170
|
+
<button id="auth-btn" class="icon-btn"><i class="fa-solid fa-user"></i></button>
|
|
171
|
+
<div id="auth-menu" class="auth-menu">
|
|
172
|
+
<div class="auth-header"><div class="auth-username" id="display-username">Client User</div><div class="auth-email" id="display-email">local@client</div></div>
|
|
173
|
+
<div class="auth-menu-item text-red-400 hover:text-red-300" id="logout-btn"><i class="fa-solid fa-right-from-bracket w-4"></i> Disconnect</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<!-- GAMES CONTENT (Originally games.html body) -->
|
|
180
|
+
<div id="games-main-container" class="games-main-container">
|
|
181
|
+
<main class="mx-auto my-5 w-full max-w-screen-2xl px-4 relative">
|
|
182
|
+
<button id="creditsBtn" class="absolute top-0 right-4 z-20 btn-toolbar-style">
|
|
183
|
+
<i class="fa-solid fa-users mr-2"></i>Credits
|
|
184
|
+
</button>
|
|
185
|
+
|
|
186
|
+
<div class="mx-auto my-8 p-4 sm:p-6 w-full max-w-3xl relative">
|
|
187
|
+
<p class="bg-card-dark text-white p-5 rounded-xl shadow-lg border border-brand-border text-sm sm:text-base text-center">
|
|
188
|
+
<strong>Welcome to 4SP Games!</strong><br>
|
|
189
|
+
This is a collection of games curated for the 4SP community. Use the search bar below to find your favorite.
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div id="favoritesSection">
|
|
194
|
+
<h2 id="favoritesHeader" class="text-center text-3xl font-bold mt-12 mb-6 text-white" style="display: none;">
|
|
195
|
+
<strong>Favorites</strong>
|
|
196
|
+
</h2>
|
|
197
|
+
<div class="games-grid grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-7 gap-4" id="favoritesGameList" style="display: none;"></div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div id="category-viewer" class="w-full mt-12">
|
|
201
|
+
<div id="category-slider-container" class="relative flex items-center justify-center max-w-sm mx-auto h-12">
|
|
202
|
+
<button id="prev-category" class="absolute left-0 top-1/2 -translate-y-1/2 text-2xl text-gray-500 hover:text-white transition-all p-2 z-10 disabled:opacity-30 disabled:text-gray-700 disabled:hover:text-gray-700">
|
|
203
|
+
<i class="fas fa-chevron-left"></i>
|
|
204
|
+
</button>
|
|
205
|
+
<div class="overflow-hidden w-64 text-center">
|
|
206
|
+
<div id="category-slider" class="flex transition-transform duration-500 ease-in-out">
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<button id="next-category" class="absolute right-0 top-1/2 -translate-y-1/2 text-2xl text-gray-500 hover:text-white transition-all p-2 z-10 disabled:opacity-30 disabled:text-gray-700 disabled:hover:text-gray-700">
|
|
210
|
+
<i class="fas fa-chevron-right"></i>
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div id="games-grid-container" class="games-grid grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-7 gap-4 mt-6 transition-opacity duration-300 ease-in-out">
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</main>
|
|
218
|
+
|
|
219
|
+
<div id="bottom-fixed-bar" class="fixed bottom-4 right-4 bg-black/80 backdrop-blur-xl rounded-3xl border border-brand-border shadow-2xl shadow-black/50 w-full max-w-md z-50 transition-all duration-300 flex flex-col">
|
|
220
|
+
<div id="searchResults" class="search-results-container w-full" style="display: none;"></div>
|
|
221
|
+
<div id="searchDivider" class="w-full h-[1px] bg-white/10" style="display: none;"></div>
|
|
222
|
+
<div class="flex items-center w-full p-1">
|
|
223
|
+
<div class="pl-4 pr-2 text-gray-400"><i class="fa-solid fa-magnifying-glass"></i></div>
|
|
224
|
+
<input type="text" id="searchInput" placeholder="Search for games…" autocomplete="off" class="w-full py-3 pr-4 bg-transparent text-white placeholder-gray-500 focus:outline-none focus:ring-0 text-base" />
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div id="zoneViewer" aria-modal="true" role="dialog">
|
|
229
|
+
<div class="zone-header">
|
|
230
|
+
<div class="zone-title"><h2 id="zoneNameEl">Game Title</h2></div>
|
|
231
|
+
<div class="zone-controls">
|
|
232
|
+
<button id="fullscreenBtnZone" title="Fullscreen"><i class="fas fa-expand"></i></button>
|
|
233
|
+
<a id="downloadBtnZone" title="Download" class="hidden" href="#"><i class="fas fa-download"></i></a>
|
|
234
|
+
<button id="closeBtnZone" title="Close"><i class="fas fa-times"></i></button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<iframe id="zoneFrame" title="Game Content" allowfullscreen sandbox="allow-scripts allow-same-origin allow-forms allow-pointer-lock"></iframe>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div id="creditsModal" class="fixed top-0 left-0 w-full h-full bg-black/70 backdrop-blur-lg z-[6000] items-center justify-center p-4" style="display: none;">
|
|
241
|
+
<div class="bg-card-dark rounded-xl border border-brand-border shadow-2xl w-full max-w-lg overflow-y-auto max-h-[90vh]">
|
|
242
|
+
<div class="flex justify-between items-center p-4 border-b border-brand-border sticky top-0 bg-card-dark z-10">
|
|
243
|
+
<h3 class="text-xl font-bold">Project Credits</h3>
|
|
244
|
+
<button id="closeCreditsBtn" class="text-white hover:text-gray-400 p-2"><i class="fas fa-times"></i></button>
|
|
245
|
+
</div>
|
|
246
|
+
<div id="creditsContent" class="p-6 space-y-6"></div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div id="instruction-overlay" class="fixed inset-0 bg-black/80 z-[8000] transition-opacity duration-300 opacity-0 flex items-center justify-center p-4" style="display: none;">
|
|
251
|
+
<div class="bg-card-dark text-white rounded-3xl border border-gray-700 shadow-2xl backdrop-blur-md w-full max-w-2xl max-h-[90vh] flex flex-col">
|
|
252
|
+
<div class="p-6 sm:p-8 overflow-y-auto custom-scrollbar">
|
|
253
|
+
<h3 class="text-2xl font-bold mb-4 text-center">Welcome to 4SP Games</h3>
|
|
254
|
+
<p class="text-sm text-gray-400 mb-6 text-center">Please read the following information before playing.</p>
|
|
255
|
+
<!-- Instructions Content (Shortened for brevity in this merge) -->
|
|
256
|
+
<ul class="space-y-4 text-gray-300 text-sm sm:text-base leading-relaxed">
|
|
257
|
+
<li>Use arrows to switch categories.</li>
|
|
258
|
+
<li>Some games may be unstable.</li>
|
|
259
|
+
<li>GN-Math games use a CDN.</li>
|
|
260
|
+
</ul>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="p-6 border-t border-gray-800 bg-black/20 rounded-b-3xl flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
263
|
+
<span class="text-xs text-gray-500">Type "I understand" to continue.</span>
|
|
264
|
+
<input type="text" id="instruction-input" placeholder="Type 'I understand'" class="w-full sm:w-auto px-6 py-3 border border-transparent text-base font-normal rounded-xl text-white bg-purple-500/10 ring-1 ring-purple-500/50 focus:ring-purple-500/80 focus:bg-purple-500/20 focus:outline-none placeholder-gray-400 transition-all duration-200">
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<!-- MAIN SCRIPT (Merged Logic) -->
|
|
271
|
+
<script type="module">
|
|
272
|
+
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
|
|
273
|
+
import { getAuth, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
|
|
274
|
+
import { getFirestore, doc, getDoc, updateDoc, setDoc, onSnapshot, collection, query, where, getDocs, serverTimestamp, arrayUnion, increment } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js";
|
|
275
|
+
|
|
276
|
+
// --- FIREBASE CONFIG ---
|
|
277
|
+
const firebaseConfig = {
|
|
278
|
+
apiKey: "AIzaSyAZBKAckVa4IMvJGjcyndZx6Y1XD52lgro",
|
|
279
|
+
authDomain: "project-zirconium.firebaseapp.com",
|
|
280
|
+
projectId: "project-zirconium",
|
|
281
|
+
storageBucket: "project-zirconium.firebasestorage.app",
|
|
282
|
+
messagingSenderId: "1096564243475",
|
|
283
|
+
appId: "1:1096564243475:web:6d0956a70125eeea1ad3e6",
|
|
284
|
+
measurementId: "G-1D4F692C1Q"
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const app = initializeApp(firebaseConfig);
|
|
288
|
+
const db = getFirestore(app);
|
|
289
|
+
const auth = getAuth(app);
|
|
290
|
+
|
|
291
|
+
// --- CONSTANTS ---
|
|
292
|
+
const STORAGE_KEY = 'local_access_code';
|
|
293
|
+
const USER_DATA_KEY = 'local_user_data';
|
|
294
|
+
const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.1.6/logged-in/';
|
|
295
|
+
const OWNER_EMAIL = "4simpleproblems@gmail.com";
|
|
296
|
+
|
|
297
|
+
// Games Config
|
|
298
|
+
const GN_ZONES_URL = "https://cdn.jsdelivr.net/gh/gn-math/assets@main/zones.json";
|
|
299
|
+
const GN_COVER_URL_BASE = "https://cdn.jsdelivr.net/gh/gn-math/covers@main";
|
|
300
|
+
const GN_HTML_URL_BASE = "https://raw.githack.com/gn-math/html/main";
|
|
301
|
+
const WORKER_ROOT = "https://dv-service-lfs.4simpleproblems.workers.dev/";
|
|
302
|
+
const GAMES_BASE_URL = "https://dv-service-lfs.4simpleproblems.workers.dev/GAMES/";
|
|
303
|
+
const CARDS_DATA_URL = "https://dv-service-lfs.4simpleproblems.workers.dev/GAMES/cards-data.js";
|
|
304
|
+
const FAVORITES_KEY = 'gameHubFavorites_v3';
|
|
305
|
+
const INSTRUCTION_KEY = '4sp-games-instruction-extended-seen';
|
|
306
|
+
|
|
307
|
+
// --- STATE ---
|
|
308
|
+
let currentUser = null;
|
|
309
|
+
let userDataUnsubscribe = null;
|
|
310
|
+
|
|
311
|
+
// Games State
|
|
312
|
+
let allGames = [];
|
|
313
|
+
let searchableGames = [];
|
|
314
|
+
let categories = [];
|
|
315
|
+
let currentCategoryIndex = 0;
|
|
316
|
+
|
|
317
|
+
// --- DOM ELEMENTS (Shell) ---
|
|
318
|
+
const lockScreen = document.getElementById('lock-screen');
|
|
319
|
+
const navbar = document.getElementById('navbar-container');
|
|
320
|
+
const gamesMainContainer = document.getElementById('games-main-container');
|
|
321
|
+
const codeInput = document.getElementById('codeInput');
|
|
322
|
+
const unlockBtn = document.getElementById('unlockBtn');
|
|
323
|
+
const lockMessage = document.getElementById('lockMessage');
|
|
324
|
+
const loadingText = document.getElementById('loading-text');
|
|
325
|
+
const authBtn = document.getElementById('auth-btn');
|
|
326
|
+
const authMenu = document.getElementById('auth-menu');
|
|
327
|
+
const displayUsername = document.getElementById('display-username');
|
|
328
|
+
const displayEmail = document.getElementById('display-email');
|
|
329
|
+
const logoutBtn = document.getElementById('logout-btn');
|
|
330
|
+
const tabsContainer = document.getElementById('tabs-container');
|
|
331
|
+
|
|
332
|
+
// --- DOM ELEMENTS (Games) ---
|
|
333
|
+
const favoritesHeader = document.getElementById("favoritesHeader");
|
|
334
|
+
const favoritesGameList = document.getElementById("favoritesGameList");
|
|
335
|
+
const searchInput = document.getElementById("searchInput");
|
|
336
|
+
const searchResults = document.getElementById("searchResults");
|
|
337
|
+
const searchDivider = document.getElementById("searchDivider");
|
|
338
|
+
const categorySlider = document.getElementById('category-slider');
|
|
339
|
+
const prevCategoryBtn = document.getElementById('prev-category');
|
|
340
|
+
const nextCategoryBtn = document.getElementById('next-category');
|
|
341
|
+
const gamesGridContainer = document.getElementById('games-grid-container');
|
|
342
|
+
const creditsModal = document.getElementById('creditsModal');
|
|
343
|
+
const creditsContent = document.getElementById('creditsContent');
|
|
344
|
+
const creditsBtn = document.getElementById('creditsBtn');
|
|
345
|
+
const closeCreditsBtn = document.getElementById('closeCreditsBtn');
|
|
346
|
+
const instructionOverlay = document.getElementById('instruction-overlay');
|
|
347
|
+
const instructionInput = document.getElementById('instruction-input');
|
|
348
|
+
const zoneViewer = document.getElementById('zoneViewer');
|
|
349
|
+
const zoneFrame = document.getElementById('zoneFrame');
|
|
350
|
+
const zoneNameEl = document.getElementById('zoneNameEl');
|
|
351
|
+
const downloadBtnZone = document.getElementById('downloadBtnZone');
|
|
352
|
+
|
|
353
|
+
// --- SHELL LOGIC ---
|
|
354
|
+
|
|
355
|
+
const CLIENT_PAGE_DATA = {
|
|
356
|
+
"games": { "name": "GAMES", "icon": "fa-solid fa-gamepad", "isLocal": true }, // Special flag for embedded
|
|
357
|
+
"dashboard": { "name": "Dashboard", "icon": "fa-solid fa-house-user", "url": "dashboard.html" },
|
|
358
|
+
"velium": { "name": "Velium", "icon": "fa-solid fa-music", "url": "velium.html" },
|
|
359
|
+
// ... add others as needed, they will open in new tabs or we could iframe them if we kept the iframe.
|
|
360
|
+
// For this test, we focus on Games working natively.
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Render Navbar
|
|
364
|
+
function renderNavbar() {
|
|
365
|
+
tabsContainer.innerHTML = '';
|
|
366
|
+
Object.entries(CLIENT_PAGE_DATA).forEach(([key, page]) => {
|
|
367
|
+
const tab = document.createElement('a');
|
|
368
|
+
tab.className = `nav-tab ${key === 'games' ? 'active' : ''}`; // Default active for this test
|
|
369
|
+
tab.innerHTML = `<i class="${page.icon}"></i> ${page.name}`;
|
|
370
|
+
tab.onclick = () => {
|
|
371
|
+
if (page.isLocal) {
|
|
372
|
+
// Switch to local view (already visible in this file)
|
|
373
|
+
console.log("Already on Games");
|
|
374
|
+
} else {
|
|
375
|
+
// Open external link
|
|
376
|
+
window.open(BASE_URL + page.url, '_blank');
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
tabsContainer.appendChild(tab);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// --- AUTH LOGIC & HELPERS ---
|
|
384
|
+
|
|
385
|
+
function updateUIWithUser(userData) {
|
|
386
|
+
if (!userData) return;
|
|
387
|
+
displayUsername.textContent = userData.username;
|
|
388
|
+
displayEmail.textContent = userData.email;
|
|
389
|
+
|
|
390
|
+
const pfpType = userData.pfpType || 'letter';
|
|
391
|
+
let pfpHtml = '';
|
|
392
|
+
if (pfpType === 'custom' && userData.customPfp) {
|
|
393
|
+
pfpHtml = `<img src="${userData.customPfp}" class="w-full h-full object-cover rounded-[14px]" alt="Profile">`;
|
|
394
|
+
} else {
|
|
395
|
+
const initial = (userData.username || 'U').charAt(0).toUpperCase();
|
|
396
|
+
const letter = userData.letterAvatarText || initial;
|
|
397
|
+
const bgColor = userData.pfpLetterBg || '#3B82F6';
|
|
398
|
+
pfpHtml = `<div class="w-full h-full rounded-[14px] flex items-center justify-center text-white font-bold text-lg" style="background-color: ${bgColor};">${letter}</div>`;
|
|
399
|
+
}
|
|
400
|
+
authBtn.innerHTML = pfpHtml;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function applyTheme(theme) {
|
|
404
|
+
if (!theme) return;
|
|
405
|
+
const root = document.documentElement;
|
|
406
|
+
if (theme['navbar-bg']) root.style.setProperty('--navbar-bg', theme['navbar-bg']);
|
|
407
|
+
if (theme['navbar-border']) root.style.setProperty('--navbar-border', theme['navbar-border']);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function fetchUserData(ownerUid) {
|
|
411
|
+
console.log("Fetching user data for:", ownerUid);
|
|
412
|
+
if (userDataUnsubscribe) userDataUnsubscribe();
|
|
413
|
+
|
|
414
|
+
return new Promise((resolve) => {
|
|
415
|
+
try {
|
|
416
|
+
userDataUnsubscribe = onSnapshot(doc(db, "users", ownerUid), (docSnap) => {
|
|
417
|
+
if (docSnap.exists()) {
|
|
418
|
+
console.log("User data snapshot received");
|
|
419
|
+
const data = docSnap.data();
|
|
420
|
+
const newUserData = {
|
|
421
|
+
...currentUser,
|
|
422
|
+
uid: ownerUid,
|
|
423
|
+
username: data.username || "User",
|
|
424
|
+
email: data.email || "No email",
|
|
425
|
+
pfpType: data.pfpType,
|
|
426
|
+
customPfp: data.customPfp,
|
|
427
|
+
pfpLetterBg: data.pfpLetterBg,
|
|
428
|
+
letterAvatarText: data.letterAvatarText,
|
|
429
|
+
navbarTheme: data.navbarTheme
|
|
430
|
+
};
|
|
431
|
+
localStorage.setItem(USER_DATA_KEY, JSON.stringify(newUserData));
|
|
432
|
+
updateUIWithUser(newUserData);
|
|
433
|
+
currentUser = newUserData;
|
|
434
|
+
applyTheme(data.navbarTheme);
|
|
435
|
+
}
|
|
436
|
+
resolve();
|
|
437
|
+
}, (error) => {
|
|
438
|
+
console.error("Snapshot error:", error);
|
|
439
|
+
resolve();
|
|
440
|
+
});
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error("User fetch error", e);
|
|
443
|
+
resolve();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ... [Middle code] ...
|
|
449
|
+
|
|
450
|
+
// Lock Screen Logic
|
|
451
|
+
async function checkAutoLogin() {
|
|
452
|
+
const savedCode = localStorage.getItem(STORAGE_KEY);
|
|
453
|
+
console.log("Checking auto-login for code:", savedCode);
|
|
454
|
+
|
|
455
|
+
if (savedCode) {
|
|
456
|
+
loadingText.classList.remove('hidden');
|
|
457
|
+
unlockBtn.classList.add('hidden');
|
|
458
|
+
codeInput.classList.add('hidden');
|
|
459
|
+
|
|
460
|
+
// Timeout fail-safe
|
|
461
|
+
const timeoutId = setTimeout(() => {
|
|
462
|
+
console.warn("Auto-login timed out");
|
|
463
|
+
resetLockScreen("Connection timed out.");
|
|
464
|
+
}, 8000); // 8s timeout
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const docRef = doc(db, "access_codes", savedCode);
|
|
468
|
+
const docSnap = await getDoc(docRef);
|
|
469
|
+
clearTimeout(timeoutId);
|
|
470
|
+
|
|
471
|
+
if (docSnap.exists() && docSnap.data().active) {
|
|
472
|
+
console.log("Code active. Owner:", docSnap.data().ownerUid);
|
|
473
|
+
const codeData = docSnap.data();
|
|
474
|
+
if (codeData.ownerUid) {
|
|
475
|
+
await fetchUserData(codeData.ownerUid);
|
|
476
|
+
}
|
|
477
|
+
console.log("Launching app...");
|
|
478
|
+
launchApp();
|
|
479
|
+
return;
|
|
480
|
+
} else {
|
|
481
|
+
console.log("Code invalid or inactive");
|
|
482
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
483
|
+
resetLockScreen("Session expired.");
|
|
484
|
+
}
|
|
485
|
+
} catch(e) {
|
|
486
|
+
clearTimeout(timeoutId);
|
|
487
|
+
console.error("Login error", e);
|
|
488
|
+
resetLockScreen("Connection failed.");
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
console.log("No saved code.");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function resetLockScreen(msg) {
|
|
496
|
+
console.log("Resetting lock screen:", msg);
|
|
497
|
+
loadingText.classList.add('hidden');
|
|
498
|
+
unlockBtn.classList.remove('hidden');
|
|
499
|
+
unlockBtn.disabled = false;
|
|
500
|
+
unlockBtn.innerText = "Access";
|
|
501
|
+
codeInput.classList.remove('hidden');
|
|
502
|
+
lockMessage.innerText = msg || "";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function launchApp() {
|
|
506
|
+
console.log("App launched!");
|
|
507
|
+
lockScreen.classList.add('hidden');
|
|
508
|
+
// Force hide just in case
|
|
509
|
+
lockScreen.style.display = 'none';
|
|
510
|
+
navbar.style.display = 'flex';
|
|
511
|
+
gamesMainContainer.style.display = 'block';
|
|
512
|
+
renderNavbar();
|
|
513
|
+
initGamesLogic();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
unlockBtn.addEventListener('click', async () => {
|
|
517
|
+
let code = codeInput.value.trim().toUpperCase().replace(/-/g, '');
|
|
518
|
+
console.log("Manual unlock attempt:", code);
|
|
519
|
+
if (code.length !== 12) { lockMessage.innerText = "Invalid format."; return; }
|
|
520
|
+
|
|
521
|
+
lockMessage.innerText = "";
|
|
522
|
+
unlockBtn.disabled = true;
|
|
523
|
+
unlockBtn.innerText = "Checking...";
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const docRef = doc(db, "access_codes", code);
|
|
527
|
+
const docSnap = await getDoc(docRef);
|
|
528
|
+
|
|
529
|
+
if (!docSnap.exists()) throw new Error("Code not found.");
|
|
530
|
+
|
|
531
|
+
const data = docSnap.data();
|
|
532
|
+
if (!data.active) throw new Error("Code inactive.");
|
|
533
|
+
|
|
534
|
+
if (data.claimed) {
|
|
535
|
+
if (localStorage.getItem(STORAGE_KEY) !== code) throw new Error("Code already used.");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!data.claimed) {
|
|
539
|
+
await updateDoc(docRef, { claimed: true });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
localStorage.setItem(STORAGE_KEY, code);
|
|
543
|
+
if (data.ownerUid) await fetchUserData(data.ownerUid);
|
|
544
|
+
launchApp();
|
|
545
|
+
} catch (e) {
|
|
546
|
+
console.error("Manual unlock failed:", e);
|
|
547
|
+
lockMessage.innerText = e.message || "Error";
|
|
548
|
+
unlockBtn.disabled = false;
|
|
549
|
+
unlockBtn.innerText = "Access";
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (authBtn) authBtn.addEventListener('click', (e) => { e.stopPropagation(); authMenu.classList.toggle('open'); });
|
|
554
|
+
document.addEventListener('click', (e) => { if (!authMenu.contains(e.target) && !authBtn.contains(e.target)) authMenu.classList.remove('open'); });
|
|
555
|
+
if (logoutBtn) logoutBtn.addEventListener('click', () => { localStorage.removeItem(STORAGE_KEY); location.reload(); });
|
|
556
|
+
|
|
557
|
+
// --- GAMES LOGIC (Merged) ---
|
|
558
|
+
|
|
559
|
+
async function fetchGameData() {
|
|
560
|
+
try {
|
|
561
|
+
const response = await fetch(CARDS_DATA_URL);
|
|
562
|
+
if (!response.ok) throw new Error('Failed to fetch game data');
|
|
563
|
+
let scriptText = await response.text();
|
|
564
|
+
if (scriptText.includes('export default')) {
|
|
565
|
+
scriptText = scriptText.replace('export default', 'window.loadedGameData =');
|
|
566
|
+
}
|
|
567
|
+
const scriptEl = document.createElement('script');
|
|
568
|
+
scriptEl.textContent = scriptText;
|
|
569
|
+
document.body.appendChild(scriptEl);
|
|
570
|
+
await new Promise(resolve => setTimeout(resolve, 50)); // Small delay for eval
|
|
571
|
+
return window.loadedGameData || [];
|
|
572
|
+
} catch (e) {
|
|
573
|
+
console.error("Error manual loading games:", e);
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function initGamesLogic() {
|
|
579
|
+
// Load Data
|
|
580
|
+
let gamesData = await fetchGameData();
|
|
581
|
+
const strongdogGames = gamesData.map(g => ({ ...g, category: 'StrongdogXP' }));
|
|
582
|
+
|
|
583
|
+
let gnMathGames = [];
|
|
584
|
+
try {
|
|
585
|
+
const res = await fetch(GN_ZONES_URL);
|
|
586
|
+
const gnMathRaw = await res.json();
|
|
587
|
+
gnMathGames = gnMathRaw.map(g => ({ id: `gn-${g.id}`, name: g.name, imgSrc: g.cover.replace("{COVER_URL}", GN_COVER_URL_BASE), category: 'GN-Math', url: g.url.replace("{HTML_URL}", GN_HTML_URL_BASE) }));
|
|
588
|
+
} catch (e) { console.warn("GN-Math load failed", e); }
|
|
589
|
+
|
|
590
|
+
// Hardcoded others (simplified)
|
|
591
|
+
const othersGames = [
|
|
592
|
+
{ id: "other-eaglercraft", name: "Eaglercraft", imgSrc: "./images/eaglercraft.png", category: "Others", url: "https://g.3kh0.net/minecraft-1.8/index.html" }, // Example external URL to avoid relative path issues in root
|
|
593
|
+
{ id: "other-doom", name: "DOOM", imgSrc: "./images/doom.png", category: "Others", url: "https://g.3kh0.net/doom/index.html" }
|
|
594
|
+
];
|
|
595
|
+
|
|
596
|
+
allGames = [...strongdogGames, ...gnMathGames, ...othersGames];
|
|
597
|
+
|
|
598
|
+
// Searchable Setup
|
|
599
|
+
searchableGames = allGames; // Simplified flat list for now
|
|
600
|
+
|
|
601
|
+
categories = ['StrongdogXP', 'GN-Math', 'Others'].filter(cat => allGames.some(g => g.category === cat));
|
|
602
|
+
|
|
603
|
+
// Render Categories
|
|
604
|
+
const categoryColors = { 'StrongdogXP': 'text-strongdog-orange', 'GN-Math': 'text-gn-math', 'Others': 'text-other-grey' };
|
|
605
|
+
categorySlider.style.width = `${categories.length * 100}%`;
|
|
606
|
+
categorySlider.innerHTML = categories.map(cat => `<div class="w-64 flex-shrink-0"><h2 class="text-3xl font-bold ${categoryColors[cat] || 'text-white'}"><strong>${cat}</strong></h2></div>`).join('');
|
|
607
|
+
|
|
608
|
+
switchCategory(0);
|
|
609
|
+
renderFavorites();
|
|
610
|
+
setupInstructionOverlay();
|
|
611
|
+
|
|
612
|
+
// Listeners
|
|
613
|
+
prevCategoryBtn.addEventListener('click', () => switchCategory(currentCategoryIndex - 1));
|
|
614
|
+
nextCategoryBtn.addEventListener('click', () => switchCategory(currentCategoryIndex + 1));
|
|
615
|
+
document.getElementById('closeBtnZone').addEventListener('click', closeZoneViewer);
|
|
616
|
+
searchInput.addEventListener("input", handleSearch);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function switchCategory(newIndex) {
|
|
620
|
+
if (newIndex < 0 || newIndex >= categories.length) return;
|
|
621
|
+
currentCategoryIndex = newIndex;
|
|
622
|
+
categorySlider.style.transform = `translateX(-${currentCategoryIndex * (100 / categories.length)}%)`;
|
|
623
|
+
|
|
624
|
+
// Render Grid
|
|
625
|
+
const categoryName = categories[currentCategoryIndex];
|
|
626
|
+
const games = allGames.filter(g => g.category === categoryName);
|
|
627
|
+
gamesGridContainer.innerHTML = '';
|
|
628
|
+
games.forEach(g => {
|
|
629
|
+
const card = createGameCard(g);
|
|
630
|
+
gamesGridContainer.appendChild(card);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
prevCategoryBtn.disabled = currentCategoryIndex === 0;
|
|
634
|
+
nextCategoryBtn.disabled = currentCategoryIndex === categories.length - 1;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function createGameCard(game) {
|
|
638
|
+
// Simplified Image Res
|
|
639
|
+
let imgSrc = game.imgSrc;
|
|
640
|
+
if (game.category === 'StrongdogXP') {
|
|
641
|
+
// Fix relative paths for root context if needed, but for now assuming remote or absolute
|
|
642
|
+
// If it's relative like ./img/..., it might fail if not in logged-in/
|
|
643
|
+
// We'll use the worker root if needed
|
|
644
|
+
if (!imgSrc.startsWith('http')) imgSrc = `${GAMES_BASE_URL}img/${imgSrc}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const card = document.createElement("div");
|
|
648
|
+
card.className = 'zone-item bg-card-dark rounded-2xl border border-brand-border overflow-hidden';
|
|
649
|
+
card.innerHTML = `
|
|
650
|
+
<div class="relative w-full cursor-pointer group">
|
|
651
|
+
<div class="aspect-w-3 aspect-h-2"><img src="${imgSrc}" alt="${game.name}" class="w-full h-full object-cover"></div>
|
|
652
|
+
<h3 class="absolute top-2 right-2 z-10 max-w-[80%] bg-black/60 backdrop-blur-md rounded-xl px-3 py-1.5 text-white truncate text-sm font-semibold shadow-lg">${game.name}</h3>
|
|
653
|
+
</div>
|
|
654
|
+
`;
|
|
655
|
+
card.onclick = () => openZone(game);
|
|
656
|
+
return card;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// --- Game Viewer ---
|
|
660
|
+
async function openZone(game) {
|
|
661
|
+
zoneNameEl.textContent = game.name;
|
|
662
|
+
zoneFrame.src = 'about:blank';
|
|
663
|
+
|
|
664
|
+
// Retry Logic
|
|
665
|
+
const attemptLoad = async (retriesLeft) => {
|
|
666
|
+
try {
|
|
667
|
+
// Direct src set for simplicity in this merged test
|
|
668
|
+
// For Strongdog/Complex URLs, we might need the embed path logic
|
|
669
|
+
// But for this test, we try direct URL first
|
|
670
|
+
let targetUrl = game.url;
|
|
671
|
+
|
|
672
|
+
// Simple URL fix for Strongdog if needed
|
|
673
|
+
if (game.category === 'StrongdogXP' && !targetUrl.startsWith('http')) {
|
|
674
|
+
targetUrl = `${WORKER_ROOT}STRONGDOG/${targetUrl}`; // Simplified assumption
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
zoneFrame.src = targetUrl;
|
|
678
|
+
|
|
679
|
+
zoneViewer.style.display = "flex";
|
|
680
|
+
zoneViewer.classList.add('active', 'animate-fade-in');
|
|
681
|
+
} catch (e) {
|
|
682
|
+
if (retriesLeft > 0) setTimeout(() => attemptLoad(retriesLeft - 1), 1000);
|
|
683
|
+
else {
|
|
684
|
+
const doc = zoneFrame.contentWindow.document;
|
|
685
|
+
doc.open(); doc.write("<h1>Load Failed</h1>"); doc.close();
|
|
686
|
+
zoneViewer.style.display = "flex";
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
attemptLoad(2);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function closeZoneViewer() {
|
|
694
|
+
zoneViewer.classList.remove('active');
|
|
695
|
+
setTimeout(() => {
|
|
696
|
+
zoneViewer.style.display = "none";
|
|
697
|
+
zoneFrame.src = 'about:blank';
|
|
698
|
+
}, 300);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Favorites (Simplified)
|
|
702
|
+
function renderFavorites() {
|
|
703
|
+
// Empty for this test, focusing on loading games without errors
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Search (Simplified)
|
|
707
|
+
function handleSearch() {
|
|
708
|
+
// ... existing logic ...
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// --- Instructions ---
|
|
712
|
+
function setupInstructionOverlay() {
|
|
713
|
+
if (localStorage.getItem(INSTRUCTION_KEY) !== 'seen') {
|
|
714
|
+
instructionOverlay.style.display = 'flex';
|
|
715
|
+
// Trigger reflow
|
|
716
|
+
setTimeout(() => {
|
|
717
|
+
instructionOverlay.classList.remove('opacity-0');
|
|
718
|
+
instructionOverlay.classList.add('opacity-100');
|
|
719
|
+
}, 10);
|
|
720
|
+
|
|
721
|
+
instructionInput.addEventListener('input', (e) => {
|
|
722
|
+
if (e.target.value.trim().toLowerCase() === 'i understand') {
|
|
723
|
+
instructionOverlay.classList.remove('opacity-100');
|
|
724
|
+
instructionOverlay.classList.add('opacity-0');
|
|
725
|
+
setTimeout(() => {
|
|
726
|
+
instructionOverlay.style.display = 'none';
|
|
727
|
+
localStorage.setItem(INSTRUCTION_KEY, 'seen');
|
|
728
|
+
}, 300);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Init
|
|
735
|
+
checkAutoLogin();
|
|
736
|
+
|
|
737
|
+
</script>
|
|
738
|
+
</body>
|
|
739
|
+
</html>
|
package/logged-in/dashboard.html
CHANGED
|
@@ -363,6 +363,39 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
|
|
|
363
363
|
</div>
|
|
364
364
|
</div>
|
|
365
365
|
|
|
366
|
+
<button id="stats-btn" class="fixed bottom-8 left-8 w-10 h-10 rounded-xl bg-black border border-[#333] text-gray-400 flex items-center justify-center hover:bg-[#111] hover:text-white hover:scale-110 transition-all duration-300 z-50">
|
|
367
|
+
<i class="fa-solid fa-chart-simple"></i>
|
|
368
|
+
</button>
|
|
369
|
+
|
|
370
|
+
<div id="stats-dropdown" class="fixed bottom-20 left-8 bg-black border border-[#333] rounded-xl p-4 min-w-[240px] opacity-0 pointer-events-none transform translate-y-4 transition-all duration-300 z-50 shadow-2xl">
|
|
371
|
+
<div class="flex items-center gap-3 mb-4 pb-3 border-b border-[#222]">
|
|
372
|
+
<div class="w-10 h-10 rounded-full bg-[#111] flex items-center justify-center text-white font-bold text-lg" id="stat-avatar">
|
|
373
|
+
?
|
|
374
|
+
</div>
|
|
375
|
+
<div>
|
|
376
|
+
<div class="text-white text-sm font-medium" id="stat-email">Loading...</div>
|
|
377
|
+
<div class="text-xs text-gray-500 font-mono" id="stat-uid">...</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div class="space-y-3">
|
|
382
|
+
<div>
|
|
383
|
+
<div class="text-[10px] uppercase tracking-wider text-gray-600 font-semibold mb-1">Member Since</div>
|
|
384
|
+
<div class="text-gray-300 text-sm" id="stat-created">...</div>
|
|
385
|
+
</div>
|
|
386
|
+
<div>
|
|
387
|
+
<div class="text-[10px] uppercase tracking-wider text-gray-600 font-semibold mb-1">Last Sign In</div>
|
|
388
|
+
<div class="text-gray-300 text-sm" id="stat-last-login">...</div>
|
|
389
|
+
</div>
|
|
390
|
+
<div>
|
|
391
|
+
<div class="text-[10px] uppercase tracking-wider text-gray-600 font-semibold mb-1">Provider</div>
|
|
392
|
+
<div class="text-gray-300 text-sm flex items-center gap-2" id="stat-provider">
|
|
393
|
+
...
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
366
399
|
<button id="retry-weather-btn" onclick="retryWeather()" title="Retry Weather">
|
|
367
400
|
<i class="fa-solid fa-cloud-sun"></i>
|
|
368
401
|
</button>
|
|
@@ -370,6 +403,59 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
|
|
|
370
403
|
<script type="module">
|
|
371
404
|
import { firebaseConfig } from '../firebase-config.js';
|
|
372
405
|
|
|
406
|
+
// --- STATS DROPDOWN LOGIC ---
|
|
407
|
+
const statsBtn = document.getElementById('stats-btn');
|
|
408
|
+
const statsDropdown = document.getElementById('stats-dropdown');
|
|
409
|
+
|
|
410
|
+
if(statsBtn && statsDropdown) {
|
|
411
|
+
statsBtn.addEventListener('click', (e) => {
|
|
412
|
+
e.stopPropagation();
|
|
413
|
+
const isVisible = statsDropdown.classList.contains('opacity-100');
|
|
414
|
+
if (isVisible) {
|
|
415
|
+
statsDropdown.classList.remove('opacity-100', 'translate-y-0', 'pointer-events-auto');
|
|
416
|
+
statsDropdown.classList.add('opacity-0', 'translate-y-4', 'pointer-events-none');
|
|
417
|
+
} else {
|
|
418
|
+
statsDropdown.classList.add('opacity-100', 'translate-y-0', 'pointer-events-auto');
|
|
419
|
+
statsDropdown.classList.remove('opacity-0', 'translate-y-4', 'pointer-events-none');
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Close when clicking outside
|
|
424
|
+
document.addEventListener('click', (e) => {
|
|
425
|
+
if (!statsDropdown.contains(e.target) && !statsBtn.contains(e.target)) {
|
|
426
|
+
statsDropdown.classList.remove('opacity-100', 'translate-y-0', 'pointer-events-auto');
|
|
427
|
+
statsDropdown.classList.add('opacity-0', 'translate-y-4', 'pointer-events-none');
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function populateStats(user) {
|
|
433
|
+
if (!user) return;
|
|
434
|
+
|
|
435
|
+
// Email & UID
|
|
436
|
+
document.getElementById('stat-email').textContent = user.email || 'Anonymous';
|
|
437
|
+
document.getElementById('stat-uid').textContent = user.uid.substring(0, 12) + '...';
|
|
438
|
+
document.getElementById('stat-avatar').textContent = (user.email || 'A').charAt(0).toUpperCase();
|
|
439
|
+
|
|
440
|
+
// Dates
|
|
441
|
+
const created = new Date(user.metadata.creationTime);
|
|
442
|
+
const lastLogin = new Date(user.metadata.lastSignInTime);
|
|
443
|
+
|
|
444
|
+
document.getElementById('stat-created').textContent = created.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
|
445
|
+
document.getElementById('stat-last-login').textContent = lastLogin.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' at ' + lastLogin.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
446
|
+
|
|
447
|
+
// Provider
|
|
448
|
+
const providerId = user.providerData[0]?.providerId || 'password';
|
|
449
|
+
let providerIcon = 'fa-envelope';
|
|
450
|
+
let providerName = 'Email';
|
|
451
|
+
|
|
452
|
+
if (providerId.includes('google')) { providerIcon = 'fa-google'; providerName = 'Google'; }
|
|
453
|
+
else if (providerId.includes('github')) { providerIcon = 'fa-github'; providerName = 'GitHub'; }
|
|
454
|
+
else if (providerId.includes('microsoft')) { providerIcon = 'fa-microsoft'; providerName = 'Microsoft'; }
|
|
455
|
+
|
|
456
|
+
document.getElementById('stat-provider').innerHTML = `<i class="fa-brands ${providerIcon}"></i> ${providerName}`;
|
|
457
|
+
}
|
|
458
|
+
|
|
373
459
|
// --- GLOBAL STATE ---
|
|
374
460
|
let CURRENT_LAT = null;
|
|
375
461
|
let CURRENT_LON = null;
|
|
@@ -709,7 +795,9 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
|
|
|
709
795
|
waitForFirebase(() => {
|
|
710
796
|
if (firebase.auth) {
|
|
711
797
|
firebase.auth().onAuthStateChanged((user) => {
|
|
712
|
-
if (
|
|
798
|
+
if (user) {
|
|
799
|
+
populateStats(user);
|
|
800
|
+
} else if (!window._LOCAL_MODE) {
|
|
713
801
|
window.location.replace(redirectPath);
|
|
714
802
|
}
|
|
715
803
|
});
|