mlv2boii 1.0.5 → 1.0.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/index.html +259 -241
- package/loader.svg +26 -40
- package/package.json +1 -1
- package/savedconfig.txt +10 -0
package/index.html
CHANGED
|
@@ -1,39 +1,47 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
6
|
<title>mlv2</title>
|
|
7
|
-
<meta name="description" content="mlv2
|
|
7
|
+
<meta name="description" content="mlv2: a small, fast library of games." />
|
|
8
8
|
<meta name="color-scheme" content="dark" />
|
|
9
|
-
<meta name="theme-color" content="#
|
|
10
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='
|
|
9
|
+
<meta name="theme-color" content="#000000" />
|
|
10
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23000'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%23e8142f'/%3E%3C/svg%3E" />
|
|
11
11
|
|
|
12
|
-
<!-- Fonts -->
|
|
12
|
+
<!-- Fonts: SF Pro on Apple devices (system stack), Inter everywhere else -->
|
|
13
13
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
14
14
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
15
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
15
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
16
16
|
|
|
17
17
|
<!-- Tailwind CSS v4 (Play CDN, mirrored on jsDelivr) -->
|
|
18
18
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
19
19
|
<style type="text/tailwindcss">
|
|
20
|
+
/*
|
|
21
|
+
Design language
|
|
22
|
+
Mood: quiet, precise, confident. Airy. Ancestor: Vercel/Resend black, Apple HIG habits.
|
|
23
|
+
Color: void black canvas, Apple grays for text, ONE red for tint (selection, primary action, focus).
|
|
24
|
+
Type: one family. 17/19/21 for reading, 13 for labels, 44-72 for the title.
|
|
25
|
+
Shape: 10px controls, 12px groups, 28px sheets (concentric: 12 + 16 padding), pills for chips,
|
|
26
|
+
22.37% on game icons (the iOS app icon ratio).
|
|
27
|
+
Motion: 150-300ms ease-out on interaction, one entrance, all of it off under reduced-motion.
|
|
28
|
+
*/
|
|
20
29
|
@theme {
|
|
21
|
-
--font-
|
|
22
|
-
--font-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
--color-ink-
|
|
26
|
-
--color-ink-
|
|
27
|
-
--color-ink-
|
|
28
|
-
--color-ink-
|
|
29
|
-
--color-ink-
|
|
30
|
-
--color-ink-
|
|
31
|
-
--color-ink-
|
|
32
|
-
--color-ink-
|
|
33
|
-
--color-ink-
|
|
34
|
-
--color-ink-
|
|
35
|
-
--color-ink-
|
|
36
|
-
--color-ink-50: #f6f5f7;
|
|
30
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
31
|
+
--font-mono: ui-monospace, "SF Mono", Menlo, monospace;
|
|
32
|
+
|
|
33
|
+
--color-ink-950: #000000;
|
|
34
|
+
--color-ink-900: #0a0a0b;
|
|
35
|
+
--color-ink-850: #101012;
|
|
36
|
+
--color-ink-800: #161618;
|
|
37
|
+
--color-ink-700: #232326;
|
|
38
|
+
--color-ink-600: #2c2c30;
|
|
39
|
+
--color-ink-500: #48484c;
|
|
40
|
+
--color-ink-400: #76767b;
|
|
41
|
+
--color-ink-300: #a1a1a6;
|
|
42
|
+
--color-ink-200: #c7c7cc;
|
|
43
|
+
--color-ink-100: #e5e5ea;
|
|
44
|
+
--color-ink-50: #f5f5f7;
|
|
37
45
|
|
|
38
46
|
--color-red-700: #8f0e20;
|
|
39
47
|
--color-red-600: #c11228;
|
|
@@ -41,147 +49,143 @@
|
|
|
41
49
|
--color-red-400: #ff4d61;
|
|
42
50
|
--color-red-300: #ff8b96;
|
|
43
51
|
}
|
|
52
|
+
|
|
53
|
+
@layer components {
|
|
54
|
+
.btn { @apply inline-flex h-11 items-center justify-center rounded-[10px] px-4 text-[15px] font-medium transition-[background-color,filter,transform] duration-200 active:scale-[0.98] disabled:opacity-50 sm:h-9 sm:text-[14px]; }
|
|
55
|
+
.btn-primary { @apply bg-red-500 text-white hover:brightness-110; }
|
|
56
|
+
.btn-ghost { @apply bg-white/10 text-ink-50 hover:bg-white/15; }
|
|
57
|
+
.icon-btn { @apply grid h-11 w-11 shrink-0 place-items-center rounded-full bg-white/10 text-ink-200 transition-colors duration-200 hover:bg-white/15 hover:text-white sm:h-9 sm:w-9; }
|
|
58
|
+
.field { @apply h-11 w-full rounded-[10px] bg-white/[0.07] px-3 text-[16px] text-ink-50 ring-1 ring-inset ring-white/10 transition-shadow duration-200 placeholder:text-ink-400 focus:ring-2 focus:ring-red-500 sm:h-9 sm:text-[14px]; }
|
|
59
|
+
.chip { @apply h-10 shrink-0 whitespace-nowrap rounded-full px-4 text-[14px] font-medium transition-colors duration-200 sm:h-9; }
|
|
60
|
+
}
|
|
44
61
|
</style>
|
|
45
62
|
|
|
46
63
|
<style>
|
|
47
|
-
|
|
48
|
-
Base layer — kept outside the Tailwind @theme block so it works
|
|
49
|
-
regardless of Tailwind's preflight timing.
|
|
50
|
-
----------------------------------------------------------------*/
|
|
51
|
-
html, body { background: var(--color-ink-950); }
|
|
64
|
+
html, body { background: #000; }
|
|
52
65
|
body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
|
|
53
|
-
::selection { background: rgba(232,20,47,0.
|
|
66
|
+
::selection { background: rgba(232,20,47,0.4); color: #fff; }
|
|
67
|
+
input { caret-color: var(--color-red-400); }
|
|
68
|
+
|
|
69
|
+
/* keyboard focus: one red ring everywhere */
|
|
70
|
+
:focus-visible { outline: 2px solid var(--color-red-400); outline-offset: 2px; }
|
|
71
|
+
input:focus-visible { outline: none; }
|
|
72
|
+
.tile:focus-visible { outline: none; }
|
|
73
|
+
.tile:focus-visible .tile-icon { outline: 2px solid var(--color-red-400); outline-offset: 3px; }
|
|
74
|
+
|
|
75
|
+
/* safe areas (viewport-fit=cover) */
|
|
76
|
+
.page-x { padding-left: max(1.25rem, env(safe-area-inset-left)); padding-right: max(1.25rem, env(safe-area-inset-right)); }
|
|
54
77
|
|
|
55
|
-
/*
|
|
56
|
-
|
|
78
|
+
/* game icons: 10% edge so dark art never melts into the canvas */
|
|
79
|
+
.tile-img { outline: 1px solid rgba(255,255,255,0.1); outline-offset: -1px; }
|
|
80
|
+
|
|
81
|
+
.no-scrollbar { scrollbar-width: none; }
|
|
82
|
+
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
83
|
+
* { scrollbar-width: thin; scrollbar-color: #2c2c30 transparent; }
|
|
57
84
|
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
58
|
-
*::-webkit-scrollbar-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
@keyframes heroLine {
|
|
82
|
-
0%, 100% { opacity: .35; transform: scaleX(.7); }
|
|
83
|
-
50% { opacity: 1; transform: scaleX(1); }
|
|
84
|
-
}
|
|
85
|
-
</style>
|
|
85
|
+
*::-webkit-scrollbar-thumb { background: #2c2c30; border-radius: 999px; }
|
|
86
|
+
|
|
87
|
+
/* the single page-load moment: title and subline rise in */
|
|
88
|
+
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
|
89
|
+
.rise { animation: rise 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation-delay: var(--d, 0s); }
|
|
90
|
+
@media (prefers-reduced-motion: reduce) { .rise { animation: none; } }
|
|
91
|
+
|
|
92
|
+
/* Blank Overlay Lock Screen */
|
|
93
|
+
#blank-overlay {
|
|
94
|
+
position: fixed;
|
|
95
|
+
inset: 0;
|
|
96
|
+
background-color: #000000;
|
|
97
|
+
z-index: 99999;
|
|
98
|
+
display: flex;
|
|
99
|
+
transition: opacity 0.4s ease, visibility 0.4s ease;
|
|
100
|
+
}
|
|
101
|
+
#blank-overlay.unlocked {
|
|
102
|
+
opacity: 0;
|
|
103
|
+
visibility: hidden;
|
|
104
|
+
pointer-events: none;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
86
107
|
</head>
|
|
87
108
|
|
|
88
|
-
<body class="
|
|
89
|
-
|
|
90
|
-
|
|
109
|
+
<body class="min-h-screen bg-ink-950 font-sans text-ink-50 antialiased">
|
|
110
|
+
|
|
111
|
+
<!-- Blank Overlay Screen -->
|
|
112
|
+
<div id="blank-overlay"></div>
|
|
91
113
|
|
|
92
114
|
<!-- ================= HEADER ================= -->
|
|
93
|
-
<header class="sticky top-0 z-40 border-b border-
|
|
94
|
-
<div class="mx-auto
|
|
95
|
-
<a href="#" class="flex items-center gap-2
|
|
96
|
-
<span class="
|
|
97
|
-
|
|
98
|
-
<span class="absolute inset-0 animate-ping rounded-full bg-red-500 opacity-60"></span>
|
|
99
|
-
</span>
|
|
100
|
-
<span class="font-display font-semibold tracking-tight text-[15px] leading-none">mlv2</span>
|
|
115
|
+
<header class="sticky top-0 z-40 border-b border-white/[0.08] bg-black/70 pt-[env(safe-area-inset-top)] backdrop-blur-xl backdrop-saturate-150">
|
|
116
|
+
<div class="page-x mx-auto flex h-14 max-w-5xl items-center gap-4">
|
|
117
|
+
<a href="#" class="flex shrink-0 items-center gap-2" aria-label="mlv2 home">
|
|
118
|
+
<span class="h-2.5 w-2.5 rounded-full bg-red-500"></span>
|
|
119
|
+
<span class="text-[17px] font-semibold tracking-[-0.02em]">mlv2</span>
|
|
101
120
|
</a>
|
|
102
121
|
|
|
103
|
-
<div class="
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
class="
|
|
122
|
+
<div class="ml-auto flex min-w-0 flex-1 items-center justify-end gap-4">
|
|
123
|
+
<span id="count-badge" class="hidden shrink-0 text-[13px] tabular-nums text-ink-400 sm:block"><span id="count-num">0</span> games</span>
|
|
124
|
+
<div class="relative w-full max-w-[16rem]">
|
|
125
|
+
<svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-ink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
|
126
|
+
<input id="search" type="text" placeholder="Search games" aria-label="Search games" autocomplete="off" enterkeyhint="search" class="field pl-9" />
|
|
127
|
+
</div>
|
|
107
128
|
</div>
|
|
108
|
-
|
|
109
|
-
<span id="count-badge" class="hidden sm:flex items-center gap-1.5 shrink-0 text-[11px] font-mono uppercase tracking-wider text-ink-400">
|
|
110
|
-
<span id="count-num">0</span> games
|
|
111
|
-
</span>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
<div class="border-t border-ink-800/60">
|
|
115
|
-
<div id="category-bar" class="mx-auto flex max-w-5xl items-center justify-center gap-1.5 overflow-x-auto px-4 py-2.5 sm:px-6"></div>
|
|
116
129
|
</div>
|
|
117
130
|
</header>
|
|
118
131
|
|
|
119
132
|
<!-- ================= MAIN ================= -->
|
|
120
|
-
<main class="
|
|
121
|
-
|
|
122
|
-
<!-- =================
|
|
123
|
-
<section class="
|
|
124
|
-
<
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
</div>
|
|
131
|
-
<h1 class="font-display text-4xl font-semibold tracking-[-0.05em] text-ink-50 sm:text-6xl">Your next <span class="text-red-400">favorite</span> game.</h1>
|
|
132
|
-
<p class="mx-auto mt-4 max-w-md text-sm leading-6 text-ink-300 sm:text-[15px]">A curated shelf of quick escapes, hidden gems, and games worth one more round.</p>
|
|
133
|
-
<div class="hero-line mx-auto mt-7 h-px w-24 bg-gradient-to-r from-transparent via-red-400 to-transparent"></div>
|
|
134
|
-
</div>
|
|
135
|
-
</section>
|
|
133
|
+
<main class="page-x mx-auto max-w-5xl pb-24">
|
|
134
|
+
|
|
135
|
+
<!-- ================= TITLE ================= -->
|
|
136
|
+
<section class="pb-10 pt-14 sm:pb-14 sm:pt-24">
|
|
137
|
+
<h1 class="rise text-balance text-[2.75rem] font-semibold leading-[1.04] tracking-[-0.04em] text-ink-50 sm:text-[4.5rem]">school sucks,<br/>games r better</h1>
|
|
138
|
+
<p class="rise mt-4 text-[19px] text-ink-300 sm:text-[21px]" style="--d: 0.08s">we're so back</p>
|
|
139
|
+
</section>
|
|
140
|
+
|
|
141
|
+
<!-- categories -->
|
|
142
|
+
<div id="category-bar" role="group" aria-label="Categories" class="no-scrollbar -mx-5 mb-8 flex items-center gap-2 overflow-x-auto px-5 sm:mx-0 sm:mb-10 sm:px-0"></div>
|
|
136
143
|
|
|
137
144
|
<!-- setup banner, shown only while CONFIG still has placeholder values -->
|
|
138
|
-
<div id="setup-banner" class="
|
|
139
|
-
<p class="text-[
|
|
140
|
-
<p class="mt-1 text-[
|
|
145
|
+
<div id="setup-banner" class="mb-8 hidden rounded-xl bg-ink-900 p-4 ring-1 ring-white/10">
|
|
146
|
+
<p class="text-[15px] font-semibold">Not connected yet</p>
|
|
147
|
+
<p class="mt-1 text-[14px] leading-relaxed text-ink-300">Open this file and fill in the <code class="font-mono text-ink-50">CONFIG</code> object near the bottom with your Appwrite endpoint, project, database, table and bucket IDs.</p>
|
|
141
148
|
</div>
|
|
142
149
|
|
|
143
150
|
<!-- skeleton loading state -->
|
|
144
|
-
<div id="skeleton-grid" class="grid grid-cols-
|
|
145
|
-
</div>
|
|
151
|
+
<div id="skeleton-grid" class="grid grid-cols-3 gap-x-4 gap-y-7 sm:grid-cols-4 sm:gap-x-5 sm:gap-y-8 md:grid-cols-5 lg:grid-cols-6"></div>
|
|
146
152
|
|
|
147
153
|
<!-- empty state -->
|
|
148
154
|
<div id="empty-state" class="hidden flex-col items-center justify-center py-24 text-center">
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
</div>
|
|
152
|
-
<p id="empty-title" class="font-display text-[15px] font-medium text-ink-100">No games yet</p>
|
|
153
|
-
<p id="empty-sub" class="mt-1.5 text-[13px] text-ink-400 max-w-xs">Games you add in Appwrite with a valid link will show up here automatically.</p>
|
|
155
|
+
<p id="empty-title" class="text-[19px] font-semibold">No games yet</p>
|
|
156
|
+
<p id="empty-sub" class="mt-2 max-w-xs text-pretty text-[15px] text-ink-400">Games you add in Appwrite with a valid link will show up here automatically.</p>
|
|
154
157
|
</div>
|
|
155
158
|
|
|
156
159
|
<!-- game grid -->
|
|
157
|
-
<div id="grid" class="hidden grid grid-cols-
|
|
160
|
+
<div id="grid" class="hidden grid grid-cols-3 gap-x-4 gap-y-7 sm:grid-cols-4 sm:gap-x-5 sm:gap-y-8 md:grid-cols-5 lg:grid-cols-6"></div>
|
|
158
161
|
</main>
|
|
159
162
|
|
|
160
163
|
<!-- ================= FOOTER ================= -->
|
|
161
|
-
<footer class="
|
|
162
|
-
<div class="mx-auto max-w-
|
|
163
|
-
<p
|
|
164
|
-
<button id="admin-trigger" class="
|
|
164
|
+
<footer class="border-t border-white/[0.08]">
|
|
165
|
+
<div class="page-x mx-auto flex max-w-5xl items-center justify-between py-4 text-[13px] text-ink-400">
|
|
166
|
+
<p>mlv2 © <span id="year"></span></p>
|
|
167
|
+
<button id="admin-trigger" class="-mr-3 rounded-lg px-3 py-2.5 transition-colors duration-200 hover:text-ink-50">Admin</button>
|
|
165
168
|
</div>
|
|
166
169
|
</footer>
|
|
167
170
|
|
|
168
171
|
<!-- ================= PLAYER MODAL ================= -->
|
|
169
172
|
<div id="player-modal" class="fixed inset-0 z-50 hidden">
|
|
170
|
-
<div class="absolute inset-0 bg-
|
|
171
|
-
<div id="player-panel" class="relative h-full w-full flex
|
|
172
|
-
<div class="
|
|
173
|
-
<div class="flex items-center gap-
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
173
|
+
<div class="absolute inset-0 bg-black" data-player-close></div>
|
|
174
|
+
<div id="player-panel" class="relative flex h-full w-full flex-col opacity-0">
|
|
175
|
+
<div class="border-b border-white/[0.08] bg-black pt-[env(safe-area-inset-top)]">
|
|
176
|
+
<div class="flex h-14 items-center justify-between gap-3 px-3 sm:px-4">
|
|
177
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
178
|
+
<img id="player-icon" class="h-8 w-8 rounded-[22.37%] object-cover" alt="" />
|
|
179
|
+
<p id="player-title" class="truncate text-[15px] font-semibold"></p>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
182
|
+
<a id="player-newtab" target="_blank" rel="noopener" title="Open in new tab" aria-label="Open in new tab" class="icon-btn">
|
|
183
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6"/><path d="M10 14 21 3"/></svg>
|
|
184
|
+
</a>
|
|
185
|
+
<button id="player-close" title="Close" aria-label="Close" class="icon-btn">
|
|
186
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
185
189
|
</div>
|
|
186
190
|
</div>
|
|
187
191
|
<div class="relative flex-1 bg-black">
|
|
@@ -192,40 +196,40 @@
|
|
|
192
196
|
|
|
193
197
|
<!-- ================= ADMIN: PIN GATE ================= -->
|
|
194
198
|
<div id="admin-login-modal" class="fixed inset-0 z-50 hidden">
|
|
195
|
-
<div class="absolute inset-0 bg-
|
|
199
|
+
<div class="absolute inset-0 bg-black/80 backdrop-blur-xl" data-admin-login-close></div>
|
|
196
200
|
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
197
|
-
<form id="admin-login-form" class="w-full max-w-[
|
|
198
|
-
<p class="
|
|
199
|
-
<p class="mt-1 text-[
|
|
201
|
+
<form id="admin-login-form" class="w-full max-w-[340px] rounded-[28px] bg-ink-900 p-6 opacity-0 ring-1 ring-white/10" data-fx>
|
|
202
|
+
<p class="text-[19px] font-semibold">Enter PIN</p>
|
|
203
|
+
<p class="mt-1.5 text-pretty text-[14px] text-ink-400">This unlocks the admin screen on this device. Write access is controlled by your Appwrite permissions.</p>
|
|
200
204
|
|
|
201
|
-
<div id="pin-boxes" class="mt-
|
|
205
|
+
<div id="pin-boxes" class="mt-5 grid grid-cols-6 gap-2">
|
|
202
206
|
<!-- 6 single-digit inputs, filled in by JS -->
|
|
203
207
|
</div>
|
|
204
208
|
<input id="admin-pin" type="hidden" />
|
|
205
209
|
|
|
206
|
-
<p id="admin-login-error" class="
|
|
210
|
+
<p id="admin-login-error" class="mt-3 hidden text-[14px] text-red-400" role="alert"></p>
|
|
207
211
|
|
|
208
|
-
<button type="submit" id="admin-login-submit" class="mt-
|
|
209
|
-
<button type="button" data-admin-login-close class="mt-2 w-full
|
|
212
|
+
<button type="submit" id="admin-login-submit" class="btn btn-primary mt-5 w-full">Unlock</button>
|
|
213
|
+
<button type="button" data-admin-login-close class="btn btn-ghost mt-2 w-full">Cancel</button>
|
|
210
214
|
</form>
|
|
211
215
|
</div>
|
|
212
216
|
</div>
|
|
213
217
|
|
|
214
218
|
<!-- ================= ADMIN: PANEL ================= -->
|
|
215
219
|
<div id="admin-panel" class="fixed inset-0 z-50 hidden">
|
|
216
|
-
<div class="absolute inset-0 bg-
|
|
217
|
-
<div class="absolute inset-0 flex items-start
|
|
218
|
-
<div id="admin-panel-card" class="relative w-full
|
|
220
|
+
<div class="absolute inset-0 bg-black/80 backdrop-blur-xl" data-admin-panel-close></div>
|
|
221
|
+
<div class="absolute inset-0 flex items-start justify-center p-0 sm:items-center sm:p-4">
|
|
222
|
+
<div id="admin-panel-card" class="relative flex h-full w-full flex-col bg-ink-900 opacity-0 sm:h-[85vh] sm:max-w-2xl sm:rounded-[32px] sm:ring-1 sm:ring-white/10" data-fx>
|
|
219
223
|
|
|
220
|
-
<div class="flex items-center justify-between
|
|
224
|
+
<div class="flex h-14 shrink-0 items-center justify-between border-b border-white/[0.08] px-5 pt-[env(safe-area-inset-top)] box-content">
|
|
221
225
|
<div>
|
|
222
|
-
<p class="
|
|
223
|
-
<p class="mt-1 text-[
|
|
226
|
+
<p class="text-[17px] font-semibold leading-none">Admin</p>
|
|
227
|
+
<p class="mt-1 text-[13px] text-ink-400">Unlocked with PIN</p>
|
|
224
228
|
</div>
|
|
225
|
-
<div class="flex items-center gap-
|
|
226
|
-
<button id="admin-logout" class="
|
|
227
|
-
<button data-admin-panel-close class="
|
|
228
|
-
<svg class="h-
|
|
229
|
+
<div class="flex items-center gap-2">
|
|
230
|
+
<button id="admin-logout" class="btn btn-ghost">Lock</button>
|
|
231
|
+
<button data-admin-panel-close aria-label="Close" class="icon-btn">
|
|
232
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
229
233
|
</button>
|
|
230
234
|
</div>
|
|
231
235
|
</div>
|
|
@@ -233,36 +237,38 @@
|
|
|
233
237
|
<div class="flex-1 overflow-y-auto px-5 py-5">
|
|
234
238
|
|
|
235
239
|
<!-- add game form -->
|
|
236
|
-
<form id="add-game-form" class="rounded-xl
|
|
237
|
-
<p class="text-[
|
|
238
|
-
<div class="mt-
|
|
240
|
+
<form id="add-game-form" class="rounded-xl p-4 ring-1 ring-white/10">
|
|
241
|
+
<p class="text-[15px] font-semibold">Add game</p>
|
|
242
|
+
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
239
243
|
<div>
|
|
240
|
-
<label class="text-[
|
|
241
|
-
<input id="new-name" required class="mt-1
|
|
244
|
+
<label for="new-name" class="text-[13px] text-ink-300">Name</label>
|
|
245
|
+
<input id="new-name" required class="field mt-1.5" />
|
|
242
246
|
</div>
|
|
243
247
|
<div>
|
|
244
|
-
<label class="text-[
|
|
245
|
-
<input id="new-category" class="mt-1
|
|
248
|
+
<label for="new-category" class="text-[13px] text-ink-300">Category (optional)</label>
|
|
249
|
+
<input id="new-category" class="field mt-1.5" />
|
|
246
250
|
</div>
|
|
247
251
|
<div class="sm:col-span-2">
|
|
248
|
-
<label class="text-[
|
|
249
|
-
<input id="new-url" required type="url" placeholder="https
|
|
252
|
+
<label for="new-url" class="text-[13px] text-ink-300">Game URL</label>
|
|
253
|
+
<input id="new-url" required type="url" placeholder="https://" class="field mt-1.5" />
|
|
250
254
|
</div>
|
|
251
255
|
<div>
|
|
252
|
-
<label class="text-[
|
|
253
|
-
<input id="new-icon-file" type="file" accept="image/*" class="mt-
|
|
256
|
+
<label for="new-icon-file" class="text-[13px] text-ink-300">Icon upload</label>
|
|
257
|
+
<input id="new-icon-file" type="file" accept="image/*" class="mt-2 w-full text-[13px] text-ink-400 file:mr-3 file:rounded-full file:border-0 file:bg-white/10 file:px-3.5 file:py-1.5 file:text-[13px] file:font-medium file:text-ink-50" />
|
|
254
258
|
</div>
|
|
255
259
|
<div>
|
|
256
|
-
<label class="text-[
|
|
257
|
-
<input id="new-icon-url" type="url" placeholder="https
|
|
260
|
+
<label for="new-icon-url" class="text-[13px] text-ink-300">Or external icon URL</label>
|
|
261
|
+
<input id="new-icon-url" type="url" placeholder="https://" class="field mt-1.5" />
|
|
258
262
|
</div>
|
|
259
263
|
</div>
|
|
260
|
-
<
|
|
261
|
-
|
|
264
|
+
<div class="mt-5 flex items-center gap-3">
|
|
265
|
+
<button type="submit" id="add-game-submit" class="btn btn-primary">Add game</button>
|
|
266
|
+
<span id="add-game-status" class="text-[13px] text-ink-400"></span>
|
|
267
|
+
</div>
|
|
262
268
|
</form>
|
|
263
269
|
|
|
264
270
|
<!-- existing games -->
|
|
265
|
-
<p class="
|
|
271
|
+
<p class="mb-3 mt-8 text-[15px] font-semibold">Games</p>
|
|
266
272
|
<div id="admin-list" class="space-y-2"></div>
|
|
267
273
|
</div>
|
|
268
274
|
</div>
|
|
@@ -270,54 +276,65 @@
|
|
|
270
276
|
</div>
|
|
271
277
|
|
|
272
278
|
<!-- ================= TOASTS ================= -->
|
|
273
|
-
<div id="toast-root" class="fixed bottom-4 right-4 z-[60] flex flex-col gap-2
|
|
279
|
+
<div id="toast-root" class="fixed bottom-4 right-4 z-[60] flex flex-col items-end gap-2 pb-[env(safe-area-inset-bottom)]"></div>
|
|
280
|
+
|
|
281
|
+
<!-- Konami Code Script with 1-Hour Expiration -->
|
|
282
|
+
<script>
|
|
283
|
+
(function() {
|
|
284
|
+
const STORAGE_KEY = 'mlv2_unlocked_expiry';
|
|
285
|
+
const ONE_HOUR_MS = 60 * 60 * 1000; // 1 hour in milliseconds
|
|
286
|
+
const overlay = document.getElementById('blank-overlay');
|
|
287
|
+
|
|
288
|
+
// Check on page load if unlock session is still valid
|
|
289
|
+
const savedExpiry = localStorage.getItem(STORAGE_KEY);
|
|
290
|
+
if (savedExpiry && Date.now() < parseInt(savedExpiry, 10)) {
|
|
291
|
+
overlay.classList.add('unlocked');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const konamiCode = [
|
|
295
|
+
'ArrowUp', 'ArrowUp',
|
|
296
|
+
'ArrowDown', 'ArrowDown',
|
|
297
|
+
'ArrowLeft', 'ArrowRight',
|
|
298
|
+
'ArrowLeft', 'ArrowRight',
|
|
299
|
+
'b', 'a', 'Enter'
|
|
300
|
+
];
|
|
301
|
+
let konamiIndex = 0;
|
|
302
|
+
|
|
303
|
+
document.addEventListener('keydown', (e) => {
|
|
304
|
+
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
305
|
+
const targetKey = konamiCode[konamiIndex].length === 1
|
|
306
|
+
? konamiCode[konamiIndex].toLowerCase()
|
|
307
|
+
: konamiCode[konamiIndex];
|
|
308
|
+
|
|
309
|
+
if (key === targetKey) {
|
|
310
|
+
konamiIndex++;
|
|
311
|
+
if (konamiIndex === konamiCode.length) {
|
|
312
|
+
// Unlock website and save expiration time for 1 hour
|
|
313
|
+
overlay.classList.add('unlocked');
|
|
314
|
+
const expiryTime = Date.now() + ONE_HOUR_MS;
|
|
315
|
+
localStorage.setItem(STORAGE_KEY, expiryTime.toString());
|
|
316
|
+
konamiIndex = 0;
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
konamiIndex = 0;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
})();
|
|
323
|
+
</script>
|
|
274
324
|
|
|
275
325
|
<script src="https://cdn.jsdelivr.net/npm/appwrite@26.2.0"></script>
|
|
276
326
|
<script type="module">
|
|
277
327
|
import { animate, stagger, inView } from "https://cdn.jsdelivr.net/npm/motion@12/+esm";
|
|
278
328
|
|
|
279
|
-
/* =====================================================================
|
|
280
|
-
mlv2 — single-file game directory
|
|
281
|
-
---------------------------------------------------------------------
|
|
282
|
-
SETUP — fill these in, that's the only thing this file needs.
|
|
283
|
-
Uses the current Appwrite TablesDB API (Appwrite console ≥ 1.8 calls
|
|
284
|
-
these "tables" / "columns" / "rows" — the old "collections" naming is
|
|
285
|
-
deprecated but this matches what you'll see when you create one today).
|
|
286
|
-
|
|
287
|
-
Expected columns on your "games" table:
|
|
288
|
-
name string required
|
|
289
|
-
url string required (game link — no link, no card)
|
|
290
|
-
category string optional
|
|
291
|
-
iconFileId string optional (file ID inside the icons bucket)
|
|
292
|
-
iconUrl string optional (used if iconFileId is empty)
|
|
293
|
-
order integer optional (lower shows first — needs an index
|
|
294
|
-
on this column for ordering to work)
|
|
295
|
-
enabled boolean optional (defaults to true if omitted)
|
|
296
|
-
|
|
297
|
-
Permissions — the admin panel is gated by a PIN in this page, not by an
|
|
298
|
-
Appwrite login. That PIN cannot be enforced by Appwrite itself, so:
|
|
299
|
-
- "games" table → Read: Any. Create/Update/Delete: Any.
|
|
300
|
-
- icons storage bucket → Read: Any. Create: Any.
|
|
301
|
-
- Add this site's origin as a Web platform in your Appwrite project.
|
|
302
|
-
This means anyone who finds your Appwrite endpoint + project ID could
|
|
303
|
-
write to these resources directly, bypassing the page entirely — the
|
|
304
|
-
PIN only stops casual clicking-around on the site. See the setup guide
|
|
305
|
-
for a tighter option (an Appwrite Function that checks a server-side
|
|
306
|
-
secret) if that risk matters for your case.
|
|
307
|
-
=====================================================================*/
|
|
308
329
|
const CONFIG = {
|
|
309
330
|
endpoint: "https://nyc.cloud.appwrite.io/v1",
|
|
310
331
|
projectId: "6a9b8a1b001b9c61537f",
|
|
311
332
|
databaseId: "6a9b8b23003de0ec5cf7",
|
|
312
|
-
gamesTableId: "6a9b8bb3000013a3fd80",
|
|
333
|
+
gamesTableId: "6a9b8bb3000013a3fd80",
|
|
313
334
|
iconsBucketId: "6a9b8b36002565ac5987",
|
|
314
335
|
};
|
|
315
336
|
|
|
316
|
-
|
|
317
|
-
// who views source, and it has no bearing on the Appwrite API itself.
|
|
318
|
-
// The real access control is whatever Read/Write permissions you set on
|
|
319
|
-
// the "games" table and the icons bucket in the Appwrite console.
|
|
320
|
-
const ADMIN_PIN = "614888";
|
|
337
|
+
const ADMIN_PIN = "051511";
|
|
321
338
|
const ADMIN_SESSION_KEY = "mlv2_admin_unlocked";
|
|
322
339
|
|
|
323
340
|
const { Client, TablesDB, Storage, Query, ID } = window.Appwrite;
|
|
@@ -331,13 +348,11 @@ const isConfigured = () =>
|
|
|
331
348
|
!CONFIG.projectId.includes("YOUR_") &&
|
|
332
349
|
!CONFIG.databaseId.includes("YOUR_");
|
|
333
350
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return animate(targets, keyframes, options);
|
|
340
|
-
}
|
|
351
|
+
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
352
|
+
function fx(targets, keyframes, options = {}) {
|
|
353
|
+
if (reduceMotion) options = { ...options, duration: 0, delay: 0 };
|
|
354
|
+
return animate(targets, keyframes, options);
|
|
355
|
+
}
|
|
341
356
|
|
|
342
357
|
/* ------------------------------- state ------------------------------- */
|
|
343
358
|
let allGames = [];
|
|
@@ -360,8 +375,8 @@ function iconSrcFor(game) {
|
|
|
360
375
|
function monogramSrc(name) {
|
|
361
376
|
const initials = (name || "?").trim().slice(0, 2).toUpperCase();
|
|
362
377
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
363
|
-
<rect width="100" height="100" fill="#
|
|
364
|
-
<text x="50" y="58" font-family="
|
|
378
|
+
<rect width="100" height="100" fill="#161618"/>
|
|
379
|
+
<text x="50" y="58" font-family="-apple-system, Inter, sans-serif" font-size="34" font-weight="600" fill="#e8142f" text-anchor="middle">${initials}</text>
|
|
365
380
|
</svg>`;
|
|
366
381
|
return "data:image/svg+xml;utf8," + encodeURIComponent(svg);
|
|
367
382
|
}
|
|
@@ -373,20 +388,20 @@ function escapeHtml(s) {
|
|
|
373
388
|
function toast(message, tone = "default") {
|
|
374
389
|
const root = el("toast-root");
|
|
375
390
|
const node = document.createElement("div");
|
|
376
|
-
const toneClasses = tone === "error" ? "
|
|
377
|
-
node.className = `rounded-
|
|
391
|
+
const toneClasses = tone === "error" ? "text-red-300" : "text-ink-50";
|
|
392
|
+
node.className = `rounded-full ${toneClasses} bg-ink-800/90 backdrop-blur-xl px-4 py-2.5 text-[14px] ring-1 ring-white/10 opacity-0`;
|
|
378
393
|
node.textContent = message;
|
|
379
394
|
root.appendChild(node);
|
|
380
|
-
fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25,
|
|
395
|
+
fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
|
|
381
396
|
setTimeout(() => {
|
|
382
|
-
fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2,
|
|
397
|
+
fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2, ease: "easeIn" }).finished.then(() => node.remove());
|
|
383
398
|
}, 2800);
|
|
384
399
|
}
|
|
385
400
|
|
|
386
401
|
/* -------------------------- render: skeleton -------------------------- */
|
|
387
402
|
function renderSkeleton() {
|
|
388
|
-
el("skeleton-grid").innerHTML = Array.from({ length:
|
|
389
|
-
.map(() => `<div class="
|
|
403
|
+
el("skeleton-grid").innerHTML = Array.from({ length: 12 })
|
|
404
|
+
.map(() => `<div class="animate-pulse motion-reduce:animate-none"><div class="aspect-square rounded-[22.37%] bg-ink-800"></div><div class="mx-auto mt-3 h-3 w-2/3 rounded-full bg-ink-800"></div></div>`)
|
|
390
405
|
.join("");
|
|
391
406
|
}
|
|
392
407
|
renderSkeleton();
|
|
@@ -406,8 +421,8 @@ function renderCategoryBar() {
|
|
|
406
421
|
bar.innerHTML = cats
|
|
407
422
|
.map((c) => {
|
|
408
423
|
const active = c === activeCategory;
|
|
409
|
-
return `<button data-cat="${escapeHtml(c)}" class="
|
|
410
|
-
active ? "
|
|
424
|
+
return `<button data-cat="${escapeHtml(c)}" aria-pressed="${active}" class="chip ${
|
|
425
|
+
active ? "bg-red-500 text-white" : "bg-white/[0.07] text-ink-300 hover:bg-white/[0.12] hover:text-ink-50"
|
|
411
426
|
}">${escapeHtml(c)}</button>`;
|
|
412
427
|
})
|
|
413
428
|
.join("");
|
|
@@ -420,6 +435,7 @@ function renderCategoryBar() {
|
|
|
420
435
|
);
|
|
421
436
|
}
|
|
422
437
|
|
|
438
|
+
let gridShown = false;
|
|
423
439
|
function renderGrid() {
|
|
424
440
|
const grid = el("grid");
|
|
425
441
|
const games = currentFiltered();
|
|
@@ -447,13 +463,12 @@ function renderGrid() {
|
|
|
447
463
|
grid.innerHTML = games
|
|
448
464
|
.map((g) => {
|
|
449
465
|
const src = iconSrcFor(g) || monogramSrc(g.name);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
</div>
|
|
466
|
+
return `<button data-fx data-id="${g.$id}" class="tile group block w-full text-left opacity-0">
|
|
467
|
+
<span class="tile-icon block aspect-square overflow-hidden rounded-[22.37%] bg-ink-800 transition-transform duration-300 ease-out group-hover:scale-[1.04] group-active:scale-[0.97]">
|
|
468
|
+
<img src="${src}" alt="" loading="lazy" class="tile-img h-full w-full object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
|
|
469
|
+
</span>
|
|
470
|
+
<span class="mt-2.5 block truncate text-center text-[13px] font-medium leading-tight text-ink-50">${escapeHtml(g.name)}</span>
|
|
471
|
+
${g.category ? `<span class="mt-0.5 block truncate text-center text-[12px] text-ink-400">${escapeHtml(g.category)}</span>` : ""}
|
|
457
472
|
</button>`;
|
|
458
473
|
})
|
|
459
474
|
.join("");
|
|
@@ -462,7 +477,10 @@ function renderGrid() {
|
|
|
462
477
|
card.addEventListener("click", () => openPlayer(games.find((g) => g.$id === card.dataset.id)))
|
|
463
478
|
);
|
|
464
479
|
|
|
465
|
-
|
|
480
|
+
// full entrance once; later filter/search changes only fade
|
|
481
|
+
fx("#grid [data-fx]", { opacity: [0, 1], y: gridShown ? 0 : [8, 0] },
|
|
482
|
+
{ duration: gridShown ? 0.18 : 0.5, delay: gridShown ? 0 : stagger(0.03), ease: "easeOut" });
|
|
483
|
+
gridShown = true;
|
|
466
484
|
}
|
|
467
485
|
|
|
468
486
|
el("count-badge") && (el("count-num").textContent = "0");
|
|
@@ -521,10 +539,10 @@ function openPlayer(game) {
|
|
|
521
539
|
const modal = el("player-modal");
|
|
522
540
|
modal.classList.remove("hidden");
|
|
523
541
|
document.documentElement.style.overflow = "hidden";
|
|
524
|
-
fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28,
|
|
542
|
+
fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28, ease: "easeOut" });
|
|
525
543
|
}
|
|
526
544
|
function closePlayer() {
|
|
527
|
-
fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18,
|
|
545
|
+
fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18, ease: "easeIn" }).finished.then(() => {
|
|
528
546
|
el("player-modal").classList.add("hidden");
|
|
529
547
|
el("player-frame").src = "";
|
|
530
548
|
document.documentElement.style.overflow = "";
|
|
@@ -543,7 +561,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
543
561
|
/* -------------------------------- admin -------------------------------- */
|
|
544
562
|
function openAdminLogin() {
|
|
545
563
|
el("admin-login-modal").classList.remove("hidden");
|
|
546
|
-
fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25,
|
|
564
|
+
fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
|
|
547
565
|
resetPinBoxes();
|
|
548
566
|
pinInputs[0]?.focus();
|
|
549
567
|
}
|
|
@@ -553,7 +571,7 @@ function closeAdminLogin() {
|
|
|
553
571
|
}
|
|
554
572
|
function openAdminPanel() {
|
|
555
573
|
el("admin-panel").classList.remove("hidden");
|
|
556
|
-
fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28,
|
|
574
|
+
fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28, ease: "easeOut" });
|
|
557
575
|
renderAdminList();
|
|
558
576
|
}
|
|
559
577
|
function closeAdminPanel() {
|
|
@@ -575,8 +593,8 @@ const pinBoxesEl = el("pin-boxes");
|
|
|
575
593
|
pinBoxesEl.innerHTML = Array.from({ length: 6 })
|
|
576
594
|
.map(
|
|
577
595
|
(_, i) =>
|
|
578
|
-
`<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1"
|
|
579
|
-
class="h-12 w-full rounded-
|
|
596
|
+
`<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1" aria-label="Digit ${i + 1}"
|
|
597
|
+
class="h-12 w-full rounded-[10px] bg-white/[0.07] text-center text-[20px] tabular-nums outline-none ring-1 ring-inset ring-white/10 transition-shadow focus:ring-2 focus:ring-red-500" />`
|
|
580
598
|
)
|
|
581
599
|
.join("");
|
|
582
600
|
const pinInputs = Array.from(pinBoxesEl.querySelectorAll("[data-pin-idx]"));
|
|
@@ -631,7 +649,7 @@ el("admin-logout").addEventListener("click", () => {
|
|
|
631
649
|
|
|
632
650
|
async function renderAdminList() {
|
|
633
651
|
const list = el("admin-list");
|
|
634
|
-
list.innerHTML = `<p class="text-[
|
|
652
|
+
list.innerHTML = `<p class="text-[14px] text-ink-400">Loading…</p>`;
|
|
635
653
|
try {
|
|
636
654
|
const res = await tablesDB.listRows({
|
|
637
655
|
databaseId: CONFIG.databaseId,
|
|
@@ -639,24 +657,24 @@ async function renderAdminList() {
|
|
|
639
657
|
queries: [Query.limit(200), Query.orderAsc("order")],
|
|
640
658
|
});
|
|
641
659
|
if (res.rows.length === 0) {
|
|
642
|
-
list.innerHTML = `<p class="text-[
|
|
660
|
+
list.innerHTML = `<p class="text-[14px] text-ink-400">No games added yet.</p>`;
|
|
643
661
|
return;
|
|
644
662
|
}
|
|
645
663
|
list.innerHTML = res.rows
|
|
646
664
|
.map(
|
|
647
665
|
(g) => `
|
|
648
|
-
<div class="flex items-center gap-3 rounded-xl
|
|
649
|
-
<img src="${iconSrcFor(g) || monogramSrc(g.name)}" class="h-
|
|
666
|
+
<div class="flex items-center gap-3 rounded-xl p-3 ring-1 ring-white/10" data-row="${g.$id}">
|
|
667
|
+
<img src="${iconSrcFor(g) || monogramSrc(g.name)}" alt="" class="tile-img h-10 w-10 shrink-0 rounded-[22.37%] object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
|
|
650
668
|
<div class="min-w-0 flex-1">
|
|
651
|
-
<input data-field="name" data-id="${g.$id}" value="${escapeHtml(g.name)}" class="w-full bg-transparent text-[
|
|
652
|
-
<input data-field="url" data-id="${g.$id}" value="${escapeHtml(g.url)}" class="mt-0.5 w-full bg-transparent text-[
|
|
669
|
+
<input data-field="name" data-id="${g.$id}" value="${escapeHtml(g.name)}" aria-label="Name" class="w-full border-b border-transparent bg-transparent text-[15px] font-medium outline-none focus:border-red-500" />
|
|
670
|
+
<input data-field="url" data-id="${g.$id}" value="${escapeHtml(g.url)}" aria-label="URL" class="mt-0.5 w-full border-b border-transparent bg-transparent text-[13px] text-ink-400 outline-none focus:border-red-500" />
|
|
653
671
|
</div>
|
|
654
|
-
<input data-field="category" data-id="${g.$id}" value="${escapeHtml(g.category || "")}" placeholder="
|
|
655
|
-
<label class="hidden
|
|
656
|
-
<input type="checkbox" data-field="enabled" data-id="${g.$id}" ${g.enabled === false ? "" : "checked"} class="accent-red-500" />
|
|
672
|
+
<input data-field="category" data-id="${g.$id}" value="${escapeHtml(g.category || "")}" placeholder="Category" aria-label="Category" class="field hidden !w-28 sm:block" />
|
|
673
|
+
<label class="hidden shrink-0 items-center gap-2 text-[13px] text-ink-300 sm:flex">
|
|
674
|
+
<input type="checkbox" data-field="enabled" data-id="${g.$id}" ${g.enabled === false ? "" : "checked"} class="h-4 w-4 accent-red-500" /> On
|
|
657
675
|
</label>
|
|
658
|
-
<button data-delete="${g.$id}" title="Delete" class="
|
|
659
|
-
<svg class="h-
|
|
676
|
+
<button data-delete="${g.$id}" title="Delete" aria-label="Delete" class="icon-btn hover:!text-red-400">
|
|
677
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6"/></svg>
|
|
660
678
|
</button>
|
|
661
679
|
</div>`
|
|
662
680
|
)
|
|
@@ -692,7 +710,7 @@ async function renderAdminList() {
|
|
|
692
710
|
})
|
|
693
711
|
);
|
|
694
712
|
} catch (err) {
|
|
695
|
-
list.innerHTML = `<p class="text-[
|
|
713
|
+
list.innerHTML = `<p class="text-[14px] text-red-400">${escapeHtml(err?.message || "Failed to load games.")}</p>`;
|
|
696
714
|
}
|
|
697
715
|
}
|
|
698
716
|
|
|
@@ -745,4 +763,4 @@ el("add-game-form").addEventListener("submit", async (e) => {
|
|
|
745
763
|
});
|
|
746
764
|
</script>
|
|
747
765
|
</body>
|
|
748
|
-
</html>
|
|
766
|
+
</html>
|
package/loader.svg
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg"
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
2
|
+
xmlns:xhtml="http://www.w3.org/1999/xhtml"
|
|
3
|
+
width="100%"
|
|
4
|
+
height="100%"
|
|
5
|
+
viewBox="0 0 1000 1000">
|
|
6
|
+
|
|
7
|
+
<foreignObject width="100%" height="100%" x="0" y="0">
|
|
8
|
+
<xhtml:html xmlns="http://www.w3.org/1999/xhtml"
|
|
9
|
+
style="width:100%;height:100%;margin:0;padding:0;">
|
|
10
|
+
|
|
4
11
|
<head>
|
|
5
12
|
<style>
|
|
6
13
|
html, body {
|
|
@@ -9,74 +16,53 @@
|
|
|
9
16
|
margin: 0;
|
|
10
17
|
padding: 0;
|
|
11
18
|
overflow: hidden;
|
|
12
|
-
background: #000;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
iframe {
|
|
16
|
-
position:
|
|
17
|
-
|
|
18
|
-
left: 0;
|
|
22
|
+
position: fixed;
|
|
23
|
+
inset: 0;
|
|
19
24
|
width: 100%;
|
|
20
25
|
height: 100%;
|
|
21
26
|
border: 0;
|
|
22
|
-
margin: 0;
|
|
23
|
-
padding: 0;
|
|
24
|
-
display: block;
|
|
25
27
|
}
|
|
26
28
|
</style>
|
|
27
29
|
</head>
|
|
28
30
|
|
|
29
31
|
<body>
|
|
30
|
-
|
|
31
32
|
<iframe id="mlv2" src="about:blank"></iframe>
|
|
32
33
|
|
|
33
34
|
<script>
|
|
34
35
|
//<![CDATA[
|
|
35
|
-
(async function
|
|
36
|
+
(async function() {
|
|
36
37
|
try {
|
|
37
38
|
const response = await fetch(
|
|
38
|
-
|
|
39
|
+
'https://cdn.jsdelivr.net/npm/mlv2boii/index.html'
|
|
39
40
|
);
|
|
40
41
|
|
|
41
42
|
if (!response.ok) {
|
|
42
|
-
throw new Error(
|
|
43
|
+
throw new Error(
|
|
44
|
+
'HTTP ' + response.status
|
|
45
|
+
);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
const html = await response.text();
|
|
46
|
-
|
|
47
|
-
const
|
|
49
|
+
|
|
50
|
+
const iframe = document.getElementById('mlv2');
|
|
51
|
+
const doc = iframe.contentDocument ||
|
|
52
|
+
iframe.contentWindow.document;
|
|
48
53
|
|
|
49
54
|
doc.open();
|
|
50
|
-
|
|
51
|
-
doc.write(`
|
|
52
|
-
<!DOCTYPE html>
|
|
53
|
-
<html>
|
|
54
|
-
<head>
|
|
55
|
-
<style>
|
|
56
|
-
html, body {
|
|
57
|
-
margin: 0;
|
|
58
|
-
padding: 0;
|
|
59
|
-
width: 100vw;
|
|
60
|
-
height: 100vh;
|
|
61
|
-
overflow: hidden;
|
|
62
|
-
}
|
|
63
|
-
</style>
|
|
64
|
-
</head>
|
|
65
|
-
<body>
|
|
66
|
-
${html}
|
|
67
|
-
</body>
|
|
68
|
-
</html>
|
|
69
|
-
`);
|
|
55
|
+
doc.write(html);
|
|
70
56
|
doc.close();
|
|
71
57
|
|
|
72
|
-
} catch (
|
|
73
|
-
console.error(
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('MLV2 loader error:', error);
|
|
74
60
|
}
|
|
75
61
|
})();
|
|
76
62
|
//]]>
|
|
77
63
|
</script>
|
|
78
|
-
|
|
79
64
|
</body>
|
|
65
|
+
|
|
80
66
|
</xhtml:html>
|
|
81
67
|
</foreignObject>
|
|
82
|
-
</svg>
|
|
68
|
+
</svg>
|
package/package.json
CHANGED
package/savedconfig.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const CONFIG = {
|
|
2
|
+
endpoint: "https://nyc.cloud.appwrite.io/v1",
|
|
3
|
+
projectId: "6a9b8a1b001b9c61537f",
|
|
4
|
+
databaseId: "6a9b8b23003de0ec5cf7",
|
|
5
|
+
gamesTableId: "6a9b8bb3000013a3fd80",
|
|
6
|
+
iconsBucketId: "6a9b8b36002565ac5987",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const ADMIN_PIN = "051511";
|
|
10
|
+
const ADMIN_SESSION_KEY = "mlv2_admin_unlocked";
|