fixed so the code compiles

This commit is contained in:
2026-07-11 11:27:43 -04:00
parent 02ac5f9593
commit 1f99268588
1183 changed files with 281990 additions and 813 deletions
Vendored
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
+2
View File
@@ -4,6 +4,7 @@ import Login from './pages/Login';
import Dashboard from './pages/Dashboard'; import Dashboard from './pages/Dashboard';
import Game from './pages/Game'; import Game from './pages/Game';
import Queue from './pages/Queue'; import Queue from './pages/Queue';
import RankedGame from './pages/RankedGame';
import Settings from './pages/Settings'; import Settings from './pages/Settings';
function getValidStoredToken() { function getValidStoredToken() {
@@ -38,6 +39,7 @@ export default function App() {
<Route path="/dashboard" element={(loggedIn || hasUrlToken()) ? <Dashboard /> : <Navigate to="/" replace />} /> <Route path="/dashboard" element={(loggedIn || hasUrlToken()) ? <Dashboard /> : <Navigate to="/" replace />} />
<Route path="/game" element={(loggedIn || hasUrlToken()) ? <Game />: <Navigate to="/" replace />} /> <Route path="/game" element={(loggedIn || hasUrlToken()) ? <Game />: <Navigate to="/" replace />} />
<Route path="/queue" element={(loggedIn || hasUrlToken()) ? <Queue /> : <Navigate to="/" replace />} /> <Route path="/queue" element={(loggedIn || hasUrlToken()) ? <Queue /> : <Navigate to="/" replace />} />
<Route path="/ranked-game" element={(loggedIn || hasUrlToken()) ? <RankedGame /> : <Navigate to="/" replace />} />
<Route path="/settings" element={(loggedIn || hasUrlToken()) ? <Settings /> : <Navigate to="/" replace />} /> <Route path="/settings" element={(loggedIn || hasUrlToken()) ? <Settings /> : <Navigate to="/" replace />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
+69
View File
@@ -0,0 +1,69 @@
import { useEffect, useRef, useState } from 'react';
const API = import.meta.env.VITE_API_URL;
const USERNAME_RE = /^[a-zA-Z0-9_]{3,20}$/;
export default function UsernameModal({ token, current, onClose, onSave }) {
const [value, setValue] = useState(current ?? '');
const [error, setError] = useState(null);
const [saving, setSaving] = useState(false);
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
async function handleSave() {
const trimmed = value.trim();
if (!trimmed) return;
if (!USERNAME_RE.test(trimmed)) {
setError('3\u201320 characters, letters, numbers, and underscores only.');
return;
}
setSaving(true);
setError(null);
try {
const res = await fetch(`${API}/me/username`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ username: trimmed }),
});
const data = await res.json();
if (res.ok) { onSave(data.username); onClose(); }
else if (data.error === 'taken') setError('Username already taken \u2014 try another.');
else setError('Something went wrong. Try again.');
} catch {
setError('Could not save. Check your connection.');
} finally {
setSaving(false);
}
}
return (
<div className="uname-overlay" onMouseDown={onClose}>
<div className="uname-modal" onMouseDown={e => e.stopPropagation()}>
<h3 className="uname-modal__title">Set username</h3>
<p className="uname-modal__hint">320 chars · letters, numbers, underscores</p>
<input
ref={inputRef}
className="uname-input"
value={value}
onChange={e => { setValue(e.target.value); setError(null); }}
onKeyDown={e => e.key === 'Enter' && handleSave()}
placeholder="e.g. player_one"
maxLength={20}
/>
{error && <p className="uname-error">{error}</p>}
<div className="uname-actions">
<button className="uname-btn uname-btn--cancel" onClick={onClose}>Cancel</button>
<button className="uname-btn uname-btn--save" onClick={handleSave} disabled={saving}>
{saving ? 'Saving\u2026' : 'Save'}
</button>
</div>
</div>
</div>
);
}
+690 -44
View File
@@ -20,9 +20,9 @@
--silver: #94a3b8; --silver: #94a3b8;
--platinum: #f5c518; --platinum: #f5c518;
--text: #e8eaf6; --text: #eceef8;
--text-muted: #4b5270; --text-muted: #7a85a8;
--text-dim: #6b7491; --text-dim: #9ba5c4;
--font-display: 'Russo One', sans-serif; --font-display: 'Russo One', sans-serif;
--font-ui: 'Barlow Condensed', sans-serif; --font-ui: 'Barlow Condensed', sans-serif;
@@ -595,16 +595,551 @@ a { color: inherit; text-decoration: none; }
/* Page wrapper */ /* Page wrapper */
.dash-page { .dash-page {
align-items: center; align-items: center;
padding: 80px 24px 80px; padding: 88px 24px 80px;
gap: 0;
}
/* Main content wrapper */
.db-wrap {
width: 100%;
max-width: 820px;
display: flex;
flex-direction: column;
gap: 0; gap: 0;
} }
/* Hero */ /* Hero */
.db-hero {
text-align: center;
padding: 44px 0 24px;
display: flex;
flex-direction: column;
align-items: center;
}
.db-hero__name {
font-family: var(--font-display);
font-size: clamp(44px, 9vw, 72px);
line-height: 1;
text-transform: uppercase;
color: var(--text);
text-shadow: 0 0 60px rgba(245,197,24,0.1);
}
/* ── CTA buttons ── */
.db-cta {
display: flex;
gap: 14px;
margin-bottom: 32px;
}
.db-btn {
flex: 1;
padding: 17px 24px;
border: none;
border-radius: var(--radius);
font-family: var(--font-display);
font-size: 18px;
letter-spacing: 0.04em;
text-transform: uppercase;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.2s;
}
.db-btn--gold {
background: var(--gold);
color: #07080f;
box-shadow: 0 0 28px rgba(245,197,24,0.3), 0 4px 14px rgba(0,0,0,0.4);
}
.db-btn--gold:hover {
transform: translateY(-2px);
box-shadow: 0 0 44px rgba(245,197,24,0.45), 0 8px 20px rgba(0,0,0,0.5);
}
.db-btn--blue {
background: transparent;
color: var(--blue-bright);
border: 1.5px solid var(--blue);
box-shadow: 0 0 20px rgba(59,130,246,0.15), 0 4px 14px rgba(0,0,0,0.3);
}
.db-btn--blue:hover {
transform: translateY(-2px);
box-shadow: 0 0 36px rgba(59,130,246,0.28), 0 8px 20px rgba(0,0,0,0.4);
}
.db-btn:active { transform: translateY(0); }
/* ── Stats strip ── */
.db-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 36px;
}
.db-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 20px 12px 18px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
position: relative;
overflow: hidden;
}
.db-stat::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(to right, transparent, var(--border-glow), transparent);
}
.db-stat__val {
font-family: var(--font-display);
font-size: clamp(26px, 4vw, 34px);
line-height: 1;
color: var(--text);
}
.db-stat__label {
font-family: var(--font-ui);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--text-dim);
}
/* ── Sections ── */
.db-section {
margin-bottom: 28px;
}
.db-section__hd {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding: 0 2px;
}
.db-section__title {
font-family: var(--font-ui);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--text-dim);
}
.lb-tabs {
display: flex;
gap: 4px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px;
}
.lb-tab {
padding: 4px 14px;
background: none;
border: none;
border-radius: 4px;
font-family: var(--font-ui);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-dim);
cursor: pointer;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.lb-tab:hover { color: var(--text); }
.lb-tab--active {
background: var(--gold-dim);
color: var(--gold);
border: 1px solid var(--gold-mid);
}
.db-section__badge {
font-family: var(--font-ui);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gold);
background: var(--gold-dim);
border: 1px solid var(--gold-mid);
border-radius: 4px;
padding: 3px 9px;
}
/* ── Tier Badge ─────────────────────────────────────────────── */
.tier-badge {
display: inline-flex;
align-items: center;
font-family: var(--font-ui);
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--tier-color);
background: color-mix(in srgb, var(--tier-color) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--tier-color) 35%, transparent);
border-radius: 4px;
box-shadow: 0 0 10px var(--tier-glow);
white-space: nowrap;
flex-shrink: 0;
}
.tier-badge--sm {
font-size: 9px;
padding: 2px 7px;
}
/* ── Tier Progress Bar ─────────────────────────────────────── */
.tier-progress {
width: 100%;
display: flex;
flex-direction: column;
gap: 6px;
}
.tier-progress__labels {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-ui);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.tier-progress__pct {
color: var(--text-muted);
font-size: 9px;
}
.tier-progress__bar {
height: 4px;
background: var(--surface-2);
border-radius: 2px;
overflow: hidden;
}
.tier-progress__fill {
height: 100%;
border-radius: 2px;
transition: width 1s cubic-bezier(0.16, 1, 0.3, 1);
}
/* ── Greeting + Hero ─────────────────────────────────────────── */
.db-greeting {
font-family: var(--font-ui);
font-size: 13px;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 6px;
}
.db-hero__namerow {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
margin-bottom: 8px;
}
.db-top-pct {
font-family: var(--font-ui);
font-size: 13px;
font-weight: 500;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-top: 4px;
}
.db-top-pct strong {
color: var(--text);
font-weight: 700;
}
/* ── Win Streak ──────────────────────────────────────────────── */
.win-streak {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 6px auto 0;
padding: 4px 14px;
background: rgba(245, 197, 24, 0.06);
border: 1px solid rgba(245, 197, 24, 0.2);
border-radius: 99px;
animation: streakPulse 2.5s ease-in-out infinite;
}
.win-streak__fire { font-size: 14px; }
.win-streak__label {
font-family: var(--font-ui);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gold);
}
@keyframes streakPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(245,197,24,0); }
50% { box-shadow: 0 0 14px 2px rgba(245,197,24,0.12); }
}
/* ── ELO Hero Block ─────────────────────────────────────────── */
.db-elo-block {
margin-bottom: 24px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px 24px;
position: relative;
overflow: hidden;
}
.db-elo-block::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(to right, transparent, var(--border-glow), transparent);
}
.db-elo-block__inner {
display: flex;
align-items: center;
gap: 28px;
}
.db-elo-main {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-shrink: 0;
gap: 2px;
}
.db-elo-val {
font-family: var(--font-display);
font-size: clamp(40px, 7vw, 58px);
line-height: 1;
}
.db-elo-label {
font-family: var(--font-ui);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--text-dim);
margin-top: 2px;
}
.db-elo-rank {
font-family: var(--font-display);
font-size: 13px;
color: var(--text-muted);
margin-top: 4px;
}
.db-elo-rank__total {
font-size: 11px;
opacity: 0.6;
}
.db-elo-block__right {
flex: 1;
min-width: 0;
}
/* ── Two-column layout ─────────────────────────────────────── */
.db-two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 28px;
}
.db-section--half {
margin-bottom: 0;
}
/* ── Match History ──────────────────────────────────────────── */
.match-history {
display: flex;
flex-direction: column;
}
.match-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
transition: background 0.15s;
}
.match-row:last-child { border-bottom: none; }
.match-row:hover { background: rgba(255,255,255,0.02); }
.match-row--win { border-left: 2px solid #22c55e; }
.match-row--loss { border-left: 2px solid #ef4444; }
.match-chip {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-display);
font-size: 13px;
flex-shrink: 0;
}
.match-chip--win { background: rgba(34,197,94,0.12); color: #22c55e; border: 1px solid rgba(34,197,94,0.3); }
.match-chip--loss { background: rgba(239,68,68,0.10); color: #ef4444; border: 1px solid rgba(239,68,68,0.3); }
.match-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.match-elo-after {
font-family: var(--font-ui);
font-size: 13px;
font-weight: 600;
color: var(--text);
letter-spacing: 0.02em;
}
.match-time {
font-family: var(--font-ui);
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.04em;
}
.match-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
flex-shrink: 0;
}
.match-elo-delta {
font-family: var(--font-display);
font-size: 15px;
}
.match-elo-delta--pos { color: #22c55e; }
.match-elo-delta--neg { color: #ef4444; }
.match-ago {
font-family: var(--font-ui);
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.04em;
}
/* ── All-grades overview ── */
.db-grades {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.db-grade-cell {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 18px 14px 16px;
display: flex;
flex-direction: column;
gap: 10px;
cursor: pointer;
transition: border-color 0.2s, transform 0.15s;
}
.db-grade-cell:hover { border-color: var(--text-muted); transform: translateY(-1px); }
.db-grade-cell--active {
border-color: var(--gold-mid);
background: var(--gold-dim);
}
.db-grade-cell__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.db-grade-cell__name {
font-family: var(--font-ui);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--text-dim);
}
.db-grade-cell__row {
display: flex;
align-items: baseline;
gap: 6px;
}
.db-grade-cell__val {
font-family: var(--font-display);
font-size: 20px;
line-height: 1;
color: var(--text);
}
.db-grade-cell__val--elo {
color: var(--gold);
text-shadow: 0 0 10px rgba(245,197,24,0.3);
}
.db-grade-cell__unit {
font-family: var(--font-ui);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-dim);
}
/* Legacy hero — kept for non-dashboard pages if any */
.dash-hero { .dash-hero {
text-align: center; text-align: center;
padding: 48px 0 36px; padding: 48px 0 36px;
width: 100%; width: 100%;
max-width: 740px; max-width: 820px;
} }
.dash-hero__eyebrow { .dash-hero__eyebrow {
@@ -670,6 +1205,8 @@ a { color: inherit; text-decoration: none; }
gap: 16px; gap: 16px;
} }
.dash-cta__btn { .dash-cta__btn {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -727,6 +1264,7 @@ a { color: inherit; text-decoration: none; }
margin-bottom: 40px; margin-bottom: 40px;
} }
.stat-card { .stat-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -776,6 +1314,7 @@ a { color: inherit; text-decoration: none; }
margin-bottom: 32px; margin-bottom: 32px;
} }
.dash-section__hd { .dash-section__hd {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -984,6 +1523,38 @@ a { color: inherit; text-decoration: none; }
color: var(--text); color: var(--text);
} }
/* Ghost (empty slot) rows */
.lb-row--ghost {
pointer-events: none;
opacity: 0.28;
}
.lb-row--ghost:hover {
background: transparent;
}
.lb-avatar--ghost {
background: var(--surface-2);
border: 1px solid var(--border);
}
.lb-ghost-bar {
display: block;
height: 9px;
width: 72px;
border-radius: 5px;
background: var(--surface-2);
}
.lb-ghost-time {
font-family: var(--font-ui);
font-size: 15px;
font-weight: 700;
letter-spacing: 0.06em;
color: var(--text-muted);
flex-shrink: 0;
}
.lb-skeleton { .lb-skeleton {
padding: 12px 16px; padding: 12px 16px;
display: flex; display: flex;
@@ -1027,6 +1598,7 @@ a { color: inherit; text-decoration: none; }
color: var(--text-muted); color: var(--text-muted);
} }
/* Error banner */ /* Error banner */
.dash-error-banner { .dash-error-banner {
display: flex; display: flex;
@@ -1061,6 +1633,8 @@ a { color: inherit; text-decoration: none; }
.dash-error-banner__dismiss:hover { opacity: 1; } .dash-error-banner__dismiss:hover { opacity: 1; }
/* Loading skeleton */ /* Loading skeleton */
.skel-pill { .skel-pill {
background: var(--surface-2); background: var(--surface-2);
@@ -1290,6 +1864,86 @@ a { color: inherit; text-decoration: none; }
color: var(--text-dim); color: var(--text-dim);
} }
.settings-row__action {
padding: 6px 16px;
background: var(--gold-dim);
border: 1px solid var(--gold-mid);
border-radius: var(--radius-sm);
color: var(--gold);
font-family: var(--font-ui);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.2s, color 0.2s, transform 0.15s;
flex-shrink: 0;
}
.settings-row__action:hover {
background: var(--gold-mid);
color: #1a1a2e;
}
.settings-row__action:active {
transform: scale(0.96);
}
.settings-avatar {
position: relative;
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
flex-shrink: 0;
border: 2px solid var(--border);
transition: border-color 0.2s, opacity 0.2s;
}
.settings-avatar:hover {
border-color: var(--gold-mid);
opacity: 0.85;
}
.settings-avatar__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.settings-avatar__placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--surface-2);
color: var(--text-muted);
}
.settings-avatar__spinner {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
border-radius: 50%;
}
.settings-avatar__spinner::after {
content: '';
position: absolute;
inset: 12px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: settings-spin 0.7s linear infinite;
}
@keyframes settings-spin {
to { transform: rotate(360deg); }
}
.settings-toggle { .settings-toggle {
position: relative; position: relative;
width: 48px; width: 48px;
@@ -1360,40 +2014,6 @@ a { color: inherit; text-decoration: none; }
color: var(--text); color: var(--text);
} }
/* ── Username nav elements ────────────────────────────────── */
.dash-nav__username {
background: none;
border: none;
font-family: var(--font-ui);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.05em;
color: var(--text-dim);
cursor: pointer;
padding: 4px 8px;
border-radius: var(--radius-sm);
transition: color 0.15s;
}
.dash-nav__username:hover { color: var(--text); }
.dash-nav__set-username {
background: none;
border: none;
font-family: var(--font-ui);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--gold);
cursor: pointer;
padding: 4px 8px;
border-radius: var(--radius-sm);
opacity: 0.75;
transition: opacity 0.15s;
}
.dash-nav__set-username:hover { opacity: 1; }
/* ── Username modal ───────────────────────────────────────── */ /* ── Username modal ───────────────────────────────────────── */
.uname-overlay { .uname-overlay {
position: fixed; position: fixed;
@@ -1514,21 +2134,47 @@ a { color: inherit; text-decoration: none; }
white-space: nowrap; white-space: nowrap;
} }
/* ── Forfeit / Quit Button ────────────────────────────────── */
.btn-forfeit {
padding: 8px 20px;
border-radius: 999px;
font-family: system-ui, sans-serif;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 1.5px solid #e57373;
background: #fff;
color: #c62828;
transition: background 0.15s, color 0.15s;
}
.btn-forfeit:hover {
background: #c62828;
color: #fff;
}
/* ── Responsive ───────────────────────────────────────────── */ /* ── Responsive ───────────────────────────────────────────── */
@media (max-width: 700px) {
.db-two-col { grid-template-columns: 1fr; }
.db-elo-block__inner { flex-direction: column; align-items: flex-start; gap: 16px; }
.db-elo-block__right { width: 100%; }
}
@media (max-width: 600px) { @media (max-width: 600px) {
.dash-nav { padding: 0 16px; } .dash-nav { padding: 0 16px; }
.dash-page { padding-left: 16px; padding-right: 16px; }
.profile-dropdown__trigger .dash-nav__name { display: none; } .profile-dropdown__trigger .dash-nav__name { display: none; }
.dash-cta__btn { padding: 16px 40px; } .db-stats { grid-template-columns: repeat(2, 1fr); }
.dash-stats { gap: 8px; } .db-stat__val { font-size: clamp(22px, 6vw, 30px); }
.stat-card { padding: 18px 10px; } .db-btn { font-size: 15px; padding: 15px 16px; }
.stat-card__value { font-size: clamp(22px, 6vw, 32px); }
.lb-row { padding: 10px 14px; gap: 10px; } .lb-row { padding: 10px 14px; gap: 10px; }
.lb-row--current { padding-left: 12px; } .lb-row--current { padding-left: 12px; }
.db-hero__namerow { flex-direction: column; gap: 8px; }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.grade-badges { gap: 8px; } .grade-badges { gap: 8px; }
.grade-badge { padding: 12px 16px; } .grade-badge { padding: 12px 16px; }
.dash-grade-tab { padding: 8px 14px; font-size: 12px; } .dash-grade-tab { padding: 8px 14px; font-size: 12px; }
.dash-hero__title { font-size: clamp(44px, 14vw, 88px); } .db-hero__name { font-size: clamp(38px, 13vw, 72px); }
.db-grades { grid-template-columns: 1fr; }
} }
+354 -219
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import ProfileDropdown from '../components/ProfileDropdown'; import ProfileDropdown from '../components/ProfileDropdown';
@@ -10,12 +10,29 @@ const GRADES = [
{ id: 'G9', label: 'Grade 9' }, { id: 'G9', label: 'Grade 9' },
]; ];
function decodeJwt(token) { const LB_SLOTS = 10;
try {
return JSON.parse(atob(token.split('.')[1])); const TIERS = [
} catch { { name: 'Bronze', min: 800, max: 899, color: '#cd7f32', glow: 'rgba(205,127,50,0.45)', next: 900 },
return null; { name: 'Silver', min: 900, max: 1099, color: '#b0bec5', glow: 'rgba(176,190,197,0.45)', next: 1100 },
{ name: 'Gold', min: 1100, max: 1299, color: '#f5c518', glow: 'rgba(245,197,24,0.55)', next: 1300 },
{ name: 'Platinum', min: 1300, max: 1499, color: '#90caf9', glow: 'rgba(144,202,249,0.5)', next: 1500 },
{ name: 'Diamond', min: 1500, max: Infinity, color: '#64ffda', glow: 'rgba(100,255,218,0.55)', next: null },
];
function getTier(elo) {
return TIERS.find(t => elo >= t.min && elo <= t.max) ?? TIERS[0];
} }
const RANK_STYLES = [
{ color: '#f5c518', textShadow: '0 0 12px rgba(245,197,24,0.6)' },
{ color: '#c0ccda', textShadow: '0 0 10px rgba(192,204,218,0.4)' },
{ color: '#e0915a', textShadow: '0 0 10px rgba(224,145,90,0.4)' },
];
function decodeJwt(token) {
try { return JSON.parse(atob(token.split('.')[1])); }
catch { return null; }
} }
function fmtTime(ms) { function fmtTime(ms) {
@@ -24,63 +41,134 @@ function fmtTime(ms) {
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
} }
function StatCard({ label, value, accent = false }) { function fmtRelative(dateStr) {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
return `${Math.floor(hrs / 24)}d ago`;
}
function getGreeting() {
const h = new Date().getHours();
if (h < 5) return 'Night owl mode';
if (h < 12) return 'Good morning';
if (h < 17) return 'Good afternoon';
return 'Good evening';
}
function useCountUp(target, duration = 900, enabled = true) {
const [value, setValue] = useState(0);
const rafRef = useRef(null);
useEffect(() => {
if (!enabled || target == null || target === 0) {
setValue(target ?? 0);
return;
}
const start = Date.now();
const tick = () => {
const elapsed = Date.now() - start;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setValue(Math.round(eased * target));
if (progress < 1) rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => cancelAnimationFrame(rafRef.current);
}, [target, duration, enabled]);
return value;
}
function TierBadge({ elo, size = 'md' }) {
const tier = getTier(elo);
const isLg = size === 'lg';
return ( return (
<div className="stat-card"> <span
<span className="stat-card__label">{label}</span> className="tier-badge"
<span className={`stat-card__value${accent ? ' stat-card__value--accent' : ''}`}> style={{
{value} '--tier-color': tier.color,
'--tier-glow': tier.glow,
fontSize: isLg ? 12 : 10,
padding: isLg ? '4px 12px' : '3px 9px',
}}
>
{tier.name}
</span> </span>
);
}
function TierProgress({ elo }) {
const tier = getTier(elo);
if (!tier.next) return null;
const pct = Math.round(((elo - tier.min) / (tier.next - tier.min)) * 100);
const nextTier = TIERS.find(t => t.min === tier.next);
return (
<div className="tier-progress">
<div className="tier-progress__labels">
<span style={{ color: tier.color }}>{tier.name}</span>
<span className="tier-progress__pct">{pct}%</span>
<span style={{ color: nextTier?.color ?? tier.color }}>{nextTier?.name}</span>
</div>
<div className="tier-progress__bar">
<div
className="tier-progress__fill"
style={{ width: `${pct}%`, background: tier.color, boxShadow: `0 0 8px ${tier.glow}` }}
/>
</div>
</div> </div>
); );
} }
const RANK_STYLES = [ function LeaderboardTable({ entries, loading, highlightRank, tab }) {
{ color: '#f5c518', textShadow: '0 0 12px rgba(245,197,24,0.6)' },
{ color: '#94a3b8', textShadow: '0 0 10px rgba(148,163,184,0.5)' },
{ color: '#c9824a', textShadow: '0 0 10px rgba(201,130,74,0.5)' },
];
function LeaderboardTable({ entries, loading, highlightRank, grade }) {
if (loading) { if (loading) {
return ( return (
<div className="lb-skeleton"> <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 8 }}>
{[...Array(5)].map((_, i) => ( {[...Array(LB_SLOTS)].map((_, i) => (
<div key={i} className="lb-skel-row" style={{ animationDelay: `${i * 0.07}s` }} /> <div key={i} className="skel-pill" style={{ height: 44, animationDelay: `${i * 0.06}s` }} />
))} ))}
</div> </div>
); );
} }
if (!entries.length) { const padded = [
const gradeLabel = GRADES.find(g => g.id === grade)?.label ?? grade; ...entries,
return ( ...[...Array(Math.max(0, LB_SLOTS - entries.length))].map((_, i) => ({
<div className="dash-empty"> rank: entries.length + i + 1,
<span className="dash-empty__icon"></span> ghost: true,
<p className="dash-empty__text">No times recorded yet for {gradeLabel}</p> })),
<p className="dash-empty__sub">Complete a quiz to appear on the board</p> ];
</div>
);
}
return ( return (
<div className="lb-table"> <div className="lb-table">
<div className="lb-header"> <div className="lb-header">
<span className="lb-header__rank">Rank</span> <span className="lb-header__rank">#</span>
<span className="lb-header__player">Player</span> <span className="lb-header__player">Player</span>
<span className="lb-header__time">Best Time</span> <span className="lb-header__time">{tab === 'elo' ? 'ELO' : 'Best Time'}</span>
</div> </div>
{entries.map(entry => { {padded.map(entry => {
if (entry.ghost) {
return (
<div key={`g${entry.rank}`} className="lb-row lb-row--ghost">
<span className="lb-rank lb-rank--num">{entry.rank}</span>
<div className="lb-user">
<div className="lb-avatar lb-avatar--ghost" />
<span className="lb-ghost-bar" />
</div>
<span className="lb-ghost-time">{tab === 'elo' ? '—' : '—:——'}</span>
</div>
);
}
const isCurrent = highlightRank !== null && entry.rank === highlightRank; const isCurrent = highlightRank !== null && entry.rank === highlightRank;
const rankStyle = entry.rank <= 3 ? RANK_STYLES[entry.rank - 1] : null; const rankStyle = entry.rank <= 3 ? RANK_STYLES[entry.rank - 1] : null;
const playerLabel = entry.username ? `@${entry.username}` : entry.displayName; const playerLabel = entry.username ? `@${entry.username}` : entry.displayName;
const fallbackLetter = (entry.username ?? entry.displayName)?.[0]?.toUpperCase() ?? '?'; const fallbackLetter = (entry.username ?? entry.displayName)?.[0]?.toUpperCase() ?? '?';
return ( return (
<div key={entry.rank} className={`lb-row${isCurrent ? ' lb-row--current' : ''}`}> <div key={entry.rank} className={`lb-row${isCurrent ? ' lb-row--current' : ''}`}>
<span <span className={`lb-rank${entry.rank > 3 ? ' lb-rank--num' : ''}`} style={rankStyle ?? undefined}>
className={`lb-rank${entry.rank > 3 ? ' lb-rank--num' : ''}`}
style={rankStyle ?? undefined}
>
{entry.rank} {entry.rank}
</span> </span>
<div className="lb-user"> <div className="lb-user">
@@ -91,7 +179,9 @@ function LeaderboardTable({ entries, loading, highlightRank, grade }) {
<span className="lb-name">{playerLabel}</span> <span className="lb-name">{playerLabel}</span>
{isCurrent && <span className="lb-you">YOU</span>} {isCurrent && <span className="lb-you">YOU</span>}
</div> </div>
<span className="lb-score lb-score--time">{fmtTime(entry.bestTime)}</span> <span className="lb-score lb-score--time">
{tab === 'elo' ? (entry.elo?.toLocaleString() ?? '—') : fmtTime(entry.bestTime)}
</span>
</div> </div>
); );
})} })}
@@ -99,63 +189,47 @@ function LeaderboardTable({ entries, loading, highlightRank, grade }) {
); );
} }
function UsernameModal({ token, current, onClose, onSave }) { function MatchHistory({ matches, loading }) {
const [value, setValue] = useState(current ?? ''); if (loading) {
const [error, setError] = useState(null);
const [saving, setSaving] = useState(false);
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
async function handleSave() {
const trimmed = value.trim();
if (!trimmed) return;
setSaving(true);
setError(null);
try {
const res = await fetch(`${API}/me/username`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ username: trimmed }),
});
const data = await res.json();
if (res.ok) { onSave(data.username); onClose(); }
else if (data.error === 'taken') setError('Username already taken — try another.');
else setError('320 characters, letters, numbers, and underscores only.');
} catch {
setError('Could not save. Check your connection.');
} finally {
setSaving(false);
}
}
return ( return (
<div className="uname-overlay" onMouseDown={onClose}> <div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '12px 16px' }}>
<div className="uname-modal" onMouseDown={e => e.stopPropagation()}> {[0,1,2].map(i => <div key={i} className="skel-pill" style={{ height: 52, animationDelay: `${i*0.07}s` }} />)}
<h3 className="uname-modal__title">Set username</h3> </div>
<p className="uname-modal__hint">320 chars · letters, numbers, underscores</p> );
<input }
ref={inputRef} if (!matches.length) {
className="uname-input" return (
value={value} <div className="dash-empty">
onChange={e => { setValue(e.target.value); setError(null); }} <div className="dash-empty__icon"></div>
onKeyDown={e => e.key === 'Enter' && handleSave()} <span className="dash-empty__text">No ranked matches yet</span>
placeholder="e.g. player_one" <span className="dash-empty__sub">Queue up to start climbing</span>
maxLength={20} </div>
/> );
{error && <p className="uname-error">{error}</p>} }
<div className="uname-actions"> return (
<button className="uname-btn uname-btn--cancel" onClick={onClose}>Cancel</button> <div className="match-history">
<button className="uname-btn uname-btn--save" onClick={handleSave} disabled={saving}> {matches.map((m, i) => {
{saving ? 'Saving…' : 'Save'} const sign = m.eloChange >= 0 ? '+' : '';
</button> return (
<div key={m.id ?? i} className={`match-row${m.won ? ' match-row--win' : ' match-row--loss'}`}>
<div className={`match-chip${m.won ? ' match-chip--win' : ' match-chip--loss'}`}>
{m.won ? 'W' : 'L'}
</div>
<div className="match-info">
<span className="match-elo-after">{m.eloAfter.toLocaleString()} ELO</span>
{m.timeSpentMs != null && (
<span className="match-time">{fmtTime(m.timeSpentMs)}</span>
)}
</div>
<div className="match-right">
<span className={`match-elo-delta${m.eloChange >= 0 ? ' match-elo-delta--pos' : ' match-elo-delta--neg'}`}>
{sign}{m.eloChange}
</span>
<span className="match-ago">{fmtRelative(m.playedAt)}</span>
</div> </div>
</div> </div>
);
})}
</div> </div>
); );
} }
@@ -166,25 +240,17 @@ function LoadingSkeleton() {
<div className="arena-bg"><div className="arena-grain" /></div> <div className="arena-bg"><div className="arena-grain" /></div>
<nav className="dash-nav"> <nav className="dash-nav">
<div className="skel-pill" style={{ width: 140, height: 22 }} /> <div className="skel-pill" style={{ width: 140, height: 22 }} />
<div className="skel-pill" style={{ width: 180, height: 36, borderRadius: 999 }} /> <div className="skel-pill" style={{ width: 40, height: 40, borderRadius: '50%' }} />
</nav> </nav>
<main className="page dash-page"> <main className="page dash-page">
<section className="dash-hero"> <div className="db-wrap">
<div className="skel-pill" style={{ width: 96, height: 12, marginBottom: 16 }} /> <div className="skel-pill" style={{ width: 280, height: 72, marginBottom: 24, alignSelf: 'center' }} />
<div className="skel-pill" style={{ width: 300, height: 64, marginBottom: 32 }} /> <div className="skel-pill" style={{ width: 240, height: 40, borderRadius: 999, marginBottom: 40, alignSelf: 'center' }} />
<div style={{ display: 'flex', gap: 8 }}> <div className="skel-pill" style={{ height: 60, marginBottom: 28 }} />
{[0, 1, 2].map(i => ( <div className="db-stats">
<div key={i} className="skel-pill" style={{ width: 96, height: 38, animationDelay: `${i * 0.08}s` }} /> {[0,1,2,3].map(i => <div key={i} className="skel-pill" style={{ height: 88, animationDelay: `${i*0.07}s` }} />)}
))}
</div> </div>
</section> <div className="skel-pill" style={{ height: 480 }} />
<div className="dash-cta">
<div className="skel-pill" style={{ width: 220, height: 58 }} />
</div>
<div className="dash-stats">
{[0, 1, 2].map(i => (
<div key={i} className="skel-pill" style={{ height: 88, animationDelay: `${i * 0.08}s` }} />
))}
</div> </div>
</main> </main>
</> </>
@@ -197,16 +263,17 @@ export default function Dashboard() {
const [jwtPayload, setJwtPayload] = useState(null); const [jwtPayload, setJwtPayload] = useState(null);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [username, setUsername] = useState(null); const [username, setUsername] = useState(null);
const [usernameModal, setUsernameModal] = useState(false);
const [stats, setStats] = useState([]); const [stats, setStats] = useState([]);
const [leaderboard, setLeaderboard] = useState([]); const [leaderboard, setLeaderboard] = useState([]);
const [matches, setMatches] = useState([]);
const [selectedGrade, setSelectedGrade] = useState('G3'); const [selectedGrade, setSelectedGrade] = useState('G3');
const [lbTab, setLbTab] = useState('elo');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [lbLoading, setLbLoading] = useState(false); const [lbLoading, setLbLoading] = useState(false);
const [matchLoading, setMatchLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const profileElo = stats.find(e => e.grade === selectedGrade)?.elo ?? null; const [statsReady, setStatsReady] = useState(false);
// ── Auth init ──
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const urlToken = params.get('token'); const urlToken = params.get('token');
@@ -222,106 +289,115 @@ export default function Dashboard() {
setJwtPayload(decodeJwt(tok)); setJwtPayload(decodeJwt(tok));
}, [navigate]); }, [navigate]);
// ── Fetch user + stats ──
useEffect(() => { useEffect(() => {
if (!token) return; if (!token) return;
const headers = { Authorization: `Bearer ${token}` }; const headers = { Authorization: `Bearer ${token}` };
Promise.all([ Promise.all([fetch(`${API}/auth/me`, { headers }), fetch(`${API}/me/stats`, { headers })])
fetch(`${API}/auth/me`, { headers }),
fetch(`${API}/me/stats`, { headers }),
])
.then(async ([meRes, statsRes]) => { .then(async ([meRes, statsRes]) => {
if (meRes.status === 401) { if (meRes.status === 401) { sessionStorage.removeItem('ranked_token'); navigate('/'); return; }
sessionStorage.removeItem('ranked_token');
setLoading(false);
navigate('/');
return;
}
const [meData, statsData] = await Promise.all([meRes.json(), statsRes.json()]); const [meData, statsData] = await Promise.all([meRes.json(), statsRes.json()]);
setUser(meData); setUser(meData);
setUsername(meData.username ?? null); setUsername(meData.username ?? null);
setStats(statsData.entries ?? []); setStats(statsData.entries ?? []);
setLoading(false); setLoading(false);
requestAnimationFrame(() => setStatsReady(true));
}) })
.catch(() => { .catch(() => { setError('Could not reach the server.'); setLoading(false); });
setError('Could not reach the server. Check your connection and refresh.');
setLoading(false);
});
}, [token, navigate]); }, [token, navigate]);
// ── Fetch leaderboard ──
useEffect(() => { useEffect(() => {
if (!token) return; if (!token) return;
setLbLoading(true); setLbLoading(true);
fetch(`${API}/leaderboard?grade=${selectedGrade}`) fetch(`${API}/leaderboard?grade=${selectedGrade}&type=${lbTab}`)
.then(res => res.json()) .then(r => r.json())
.then(data => setLeaderboard(Array.isArray(data) ? data : [])) .then(d => setLeaderboard(Array.isArray(d) ? d : []))
.catch(() => setLeaderboard([])) .catch(() => setLeaderboard([]))
.finally(() => setLbLoading(false)); .finally(() => setLbLoading(false));
}, [token, selectedGrade, lbTab]);
useEffect(() => {
if (!token) return;
setMatchLoading(true);
fetch(`${API}/me/recent-matches?grade=${selectedGrade}`, {
headers: { Authorization: `Bearer ${token}` },
})
.then(r => r.json())
.then(d => setMatches(Array.isArray(d) ? d : []))
.catch(() => setMatches([]))
.finally(() => setMatchLoading(false));
}, [token, selectedGrade]); }, [token, selectedGrade]);
const signOut = () => { const signOut = () => { sessionStorage.removeItem('ranked_token'); navigate('/'); };
sessionStorage.removeItem('ranked_token');
navigate('/');
};
const currentEntry = stats.find(e => e.grade === selectedGrade); const currentEntry = stats.find(e => e.grade === selectedGrade);
const highlightRank = currentEntry?.timeRank ?? null; const highlightRank = lbTab === 'elo' ? (currentEntry?.eloRank ?? null) : (currentEntry?.timeRank ?? null);
const displayName = user?.displayName ?? jwtPayload?.name ?? 'Player'; const displayName = user?.displayName ?? jwtPayload?.name ?? 'Player';
const firstName = displayName.split(' ')[0]; const firstName = displayName.split(' ')[0];
const avatarUrl = user?.avatarUrl ?? jwtPayload?.picture ?? null; const avatarUrl = user?.avatarUrl ?? jwtPayload?.picture ?? null;
const initial = displayName[0]?.toUpperCase() ?? '?'; const initial = displayName[0]?.toUpperCase() ?? '?';
const elo = currentEntry?.elo ?? 800;
const tier = getTier(elo);
const winStreak = currentEntry?.winStreak ?? 0;
const eloRank = currentEntry?.eloRank ?? null;
const totalPlayers = currentEntry?.totalPlayers ?? 0;
const gamesPlayed = currentEntry?.gamesPlayed ?? 0;
const topPct = eloRank && totalPlayers > 0
? Math.max(1, Math.round((eloRank / totalPlayers) * 100))
: null;
const animatedElo = useCountUp(elo, 1000, statsReady);
const animatedGames = useCountUp(gamesPlayed, 800, statsReady);
if (loading) return <LoadingSkeleton />; if (loading) return <LoadingSkeleton />;
const heroLabel = username ?? firstName;
const casualLabel = gamesPlayed === 0 ? 'Play First Game' : 'Casual';
return ( return (
<> <>
<div className="arena-bg"><div className="arena-grain" /></div> <div className="arena-bg"><div className="arena-grain" /></div>
{usernameModal && (
<UsernameModal
token={token}
current={username}
onClose={() => setUsernameModal(false)}
onSave={(u) => setUsername(u)}
/>
)}
{/* ── Nav ── */}
<nav className="dash-nav"> <nav className="dash-nav">
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span className="dash-nav__logo">RANKED<span>EQAO</span></span> <span className="dash-nav__logo">RANKED<span>EQAO</span></span>
{profileElo !== null && <span style={{
fontFamily: 'var(--font-ui)', fontSize: 13, fontWeight: 600,
color: 'var(--gold)', letterSpacing: '0.04em',
padding: '4px 12px', borderRadius: 999,
background: 'var(--gold-dim)',
border: '1px solid var(--gold-mid)',
}}>
ELO: {profileElo.toLocaleString()}
</span>}
</div>
<div className="dash-nav__right"> <div className="dash-nav__right">
{username <ProfileDropdown displayName={displayName} avatarUrl={avatarUrl} initial={initial} onSignOut={signOut} />
? <button className="dash-nav__username" onClick={() => setUsernameModal(true)}>@{username}</button>
: <button className="dash-nav__set-username" onClick={() => setUsernameModal(true)}>Set username</button>
}
<ProfileDropdown
displayName={displayName}
avatarUrl={avatarUrl}
initial={initial}
onSignOut={signOut}
/>
</div> </div>
</nav> </nav>
<main className="page dash-page"> <main className="page dash-page">
<div className="db-wrap">
{error && (
<div className="dash-error-banner" style={{ maxWidth: '100%', marginBottom: 24 }}>
<span>{error}</span>
<button className="dash-error-banner__dismiss" onClick={() => setError(null)}></button>
</div>
)}
{/* ── Hero ── */} {/* ── Hero ── */}
<section className="dash-hero"> <section className="db-hero">
<h1 className="dash-hero__title">{username ?? firstName}</h1> <p className="db-greeting">{getGreeting()},</p>
<div className="dash-grade-tabs"> <div className="db-hero__namerow">
<h1 className="db-hero__name">{heroLabel}</h1>
<TierBadge elo={elo} size="lg" />
</div>
{winStreak >= 2 && (
<div className="win-streak">
<span className="win-streak__fire">🔥</span>
<span className="win-streak__label">{winStreak} win streak</span>
</div>
)}
{topPct && (
<p className="db-top-pct">
Top <strong>{topPct}%</strong> in Grade {selectedGrade.replace('G', '')}
</p>
)}
<div className="dash-grade-tabs" style={{ marginTop: 20 }}>
{GRADES.map(g => ( {GRADES.map(g => (
<button <button
key={g.id} key={g.id}
@@ -334,71 +410,130 @@ export default function Dashboard() {
</div> </div>
</section> </section>
{/* ── CTA Buttons ── */} {/* ── CTA buttons ── */}
<div className="dash-cta"> <div className="db-cta">
<button className="dash-cta__btn" onClick={() => navigate('/game', { state: { grade: selectedGrade } })}> <button className="db-btn db-btn--gold" onClick={() => navigate('/game', { state: { grade: selectedGrade } })}>
<span className="dash-cta__label">Start Game</span> {casualLabel}
</button> </button>
<button className="dash-cta__btn dash-cta__btn--ranked" onClick={() => navigate('/queue', { state: { grade: selectedGrade } })}> <button className="db-btn db-btn--blue" onClick={() => navigate('/queue', { state: { grade: selectedGrade } })}>
<span className="dash-cta__label">Queue Ranked</span> Queue Ranked
</button> </button>
</div> </div>
{/* ── Error banner ── */} {/* ── ELO + Tier progress ── */}
{error && ( <div className="db-elo-block">
<div className="dash-error-banner"> <div className="db-elo-block__inner">
<span>{error}</span> <div className="db-elo-main">
<button className="dash-error-banner__dismiss" onClick={() => setError(null)}></button> <span className="db-elo-val" style={{ color: tier.color, textShadow: `0 0 24px ${tier.glow}` }}>
</div> {animatedElo.toLocaleString()}
</span>
<span className="db-elo-label">ELO</span>
{eloRank && (
<span className="db-elo-rank">
#{eloRank}
{totalPlayers > 0 && <span className="db-elo-rank__total"> / {totalPlayers}</span>}
</span>
)} )}
</div>
{/* ── Stats row ── */} <div className="db-elo-block__right">
<div className="dash-stats"> <TierProgress elo={elo} />
<StatCard </div>
label="Best Time" </div>
value={fmtTime(currentEntry?.bestTime)}
accent
/>
<StatCard
label="Games Played"
value={currentEntry?.gamesPlayed ?? '—'}
/>
<StatCard
label="ELO"
value={currentEntry?.elo?.toLocaleString() ?? '—'}
/>
</div> </div>
{/* ── Leaderboard ── */} {/* ── Stats strip ── */}
<section className="dash-section"> <div className="db-stats">
<div className="dash-section__hd"> {[
<h2 className="dash-section__title">Leaderboard</h2> { label: 'Best Time', value: fmtTime(currentEntry?.bestTime), accent: true },
<span className="dash-section__badge">Best Time</span> { label: 'Games', value: animatedGames },
{ label: 'Correct', value: currentEntry?.totalCorrect ?? 0 },
{ label: 'Time Rank', value: currentEntry?.timeRank != null ? `#${currentEntry.timeRank}` : '—' },
].map(s => (
<div key={s.label} className="db-stat">
<span className="db-stat__val" style={s.accent ? { color: 'var(--gold)', textShadow: '0 0 18px rgba(245,197,24,0.35)' } : {}}>
{s.value}
</span>
<span className="db-stat__label">{s.label}</span>
</div>
))}
</div>
{/* ── Recent Matches + Leaderboard side by side ── */}
<div className="db-two-col">
{/* Recent Matches */}
<section className="db-section db-section--half">
<div className="db-section__hd">
<span className="db-section__title">Recent Matches</span>
{winStreak >= 2 && (
<span className="db-section__badge" style={{ background: 'rgba(245,197,24,0.08)', borderColor: 'rgba(245,197,24,0.3)', color: 'var(--gold)' }}>
🔥 {winStreak} streak
</span>
)}
</div> </div>
<div className="dash-card"> <div className="dash-card">
<LeaderboardTable <MatchHistory matches={matches} loading={matchLoading} />
entries={leaderboard}
loading={lbLoading}
highlightRank={highlightRank}
grade={selectedGrade}
/>
</div> </div>
</section> </section>
{/* ── Recent Activity ── */} {/* Leaderboard */}
<section className="dash-section"> <section className="db-section db-section--half">
<div className="dash-section__hd"> <div className="db-section__hd">
<h2 className="dash-section__title">Recent Activity</h2> <span className="db-section__title">Leaderboard</span>
<div className="lb-tabs">
<button className={`lb-tab${lbTab === 'elo' ? ' lb-tab--active' : ''}`} onClick={() => setLbTab('elo')}>ELO</button>
<button className={`lb-tab${lbTab === 'time' ? ' lb-tab--active' : ''}`} onClick={() => setLbTab('time')}>Time</button>
</div>
</div> </div>
<div className="dash-card"> <div className="dash-card">
<div className="dash-empty"> <LeaderboardTable entries={leaderboard} loading={lbLoading} highlightRank={highlightRank} tab={lbTab} />
<span className="dash-empty__icon">🎮</span>
<p className="dash-empty__text">No games played yet</p>
<p className="dash-empty__sub">Complete a game to see your activity here</p>
</div>
</div> </div>
</section> </section>
</div>
{/* ── All-grades overview ── */}
<section className="db-section">
<div className="db-section__hd">
<span className="db-section__title">All Grades</span>
</div>
<div className="db-grades">
{GRADES.map(g => {
const e = stats.find(s => s.grade === g.id);
const t = getTier(e?.elo ?? 800);
return (
<div
key={g.id}
className={`db-grade-cell${selectedGrade === g.id ? ' db-grade-cell--active' : ''}`}
onClick={() => setSelectedGrade(g.id)}
>
<div className="db-grade-cell__top">
<span className="db-grade-cell__name">{g.label}</span>
<span className="tier-badge tier-badge--sm" style={{ '--tier-color': t.color, '--tier-glow': t.glow }}>
{t.name}
</span>
</div>
<div className="db-grade-cell__row">
<span className="db-grade-cell__val db-grade-cell__val--elo" style={{ color: t.color, textShadow: `0 0 10px ${t.glow}` }}>
{e?.elo?.toLocaleString() ?? '800'}
</span>
<span className="db-grade-cell__unit">ELO</span>
</div>
<div className="db-grade-cell__row">
<span className="db-grade-cell__val">{fmtTime(e?.bestTime)}</span>
<span className="db-grade-cell__unit">best</span>
</div>
<div className="db-grade-cell__row">
<span className="db-grade-cell__val">{e?.gamesPlayed ?? 0}</span>
<span className="db-grade-cell__unit">games</span>
</div>
</div>
);
})}
</div>
</section>
</div>
</main> </main>
</> </>
); );
+42 -25
View File
@@ -63,6 +63,12 @@ const NAV_BTN = {
border: '1.5px solid #b6d8f5', background: '#e8f4fd', color: '#1a73e8', border: '1.5px solid #b6d8f5', background: '#e8f4fd', color: '#1a73e8',
}; };
const QUIT_BTN = {
padding: '14px 28px', borderRadius: 999, fontFamily: 'system-ui,sans-serif',
fontSize: 14, fontWeight: 600, cursor: 'pointer',
border: '1.5px solid #e57373', background: '#fff', color: '#c62828',
};
const WRAP = { display: 'flex', flexDirection: 'column', minHeight: '100vh', background: '#fff', fontFamily: 'system-ui,sans-serif' }; const WRAP = { display: 'flex', flexDirection: 'column', minHeight: '100vh', background: '#fff', fontFamily: 'system-ui,sans-serif' };
// ── Screens ──────────────────────────────────────────────────── // ── Screens ────────────────────────────────────────────────────
@@ -89,7 +95,7 @@ function StageIntro({ stage, onNext }) {
); );
} }
function QuestionScreen({ question: q, qIdx, answers, selected, onSelect, onNext, onBack, onJump }) { function QuestionScreen({ question: q, qIdx, answers, selected, onSelect, onNext, onBack, onJump, onQuit }) {
useEffect(() => { useEffect(() => {
const handler = (e) => { const handler = (e) => {
const i = +e.key - 1; const i = +e.key - 1;
@@ -147,7 +153,9 @@ function QuestionScreen({ question: q, qIdx, answers, selected, onSelect, onNext
</div> </div>
</div> </div>
<div style={{ borderTop: '1px solid #e0e0e0', boxShadow: '0 -2px 8px rgba(0,0,0,0.06)', padding: '16px 32px', display: 'flex', justifyContent: 'flex-end', gap: 14 }}> <div style={{ borderTop: '1px solid #e0e0e0', boxShadow: '0 -2px 8px rgba(0,0,0,0.06)', padding: '16px 32px', display: 'flex', justifyContent: 'space-between', gap: 14 }}>
<button style={QUIT_BTN} onClick={onQuit}>Quit</button>
<div style={{ display: 'flex', gap: 14 }}>
{qIdx > 0 && <button style={NAV_BTN} onClick={onBack}> Back</button>} {qIdx > 0 && <button style={NAV_BTN} onClick={onBack}> Back</button>}
{qIdx === STAGE_SIZE - 1 ? ( {qIdx === STAGE_SIZE - 1 ? (
<button style={{ padding: '14px 40px', borderRadius: 999, fontSize: 17, fontWeight: 700, cursor: 'pointer', border: 'none', fontFamily: 'system-ui,sans-serif', background: '#ff69b4', color: '#000', boxShadow: '0 0 6px #ff69b4aa' }} onClick={onNext}> <button style={{ padding: '14px 40px', borderRadius: 999, fontSize: 17, fontWeight: 700, cursor: 'pointer', border: 'none', fontFamily: 'system-ui,sans-serif', background: '#ff69b4', color: '#000', boxShadow: '0 0 6px #ff69b4aa' }} onClick={onNext}>
@@ -159,6 +167,7 @@ function QuestionScreen({ question: q, qIdx, answers, selected, onSelect, onNext
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }
@@ -256,22 +265,7 @@ function StageResult({ stage, wrongCount, questions, answers: initialAnswers, on
); );
} }
function CompleteScreen({ duration, grade, onDashboard }) { function CompleteScreen({ duration, isNewBest, onDashboard }) {
const [pbState, setPbState] = useState(null);
useEffect(() => {
const token = sessionStorage.getItem('ranked_token');
if (!token || !grade) { setPbState({ updated: false }); return; }
fetch(`${API}/me/best-time`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ grade, timeMs: duration }),
})
.then(r => r.json())
.then(data => setPbState({ updated: data.updated ?? false }))
.catch(() => setPbState({ updated: false }));
}, []);
return ( return (
<div style={WRAP}> <div style={WRAP}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px 32px', textAlign: 'center', gap: 20 }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px 32px', textAlign: 'center', gap: 20 }}>
@@ -281,9 +275,9 @@ function CompleteScreen({ duration, grade, onDashboard }) {
<div style={{ padding: '14px 40px', background: '#f5f7ff', border: '2px solid #c5d3f8', borderRadius: 12, fontSize: 24, fontWeight: 700, color: '#1a73e8' }}> <div style={{ padding: '14px 40px', background: '#f5f7ff', border: '2px solid #c5d3f8', borderRadius: 12, fontSize: 24, fontWeight: 700, color: '#1a73e8' }}>
{fmtDuration(duration)} {fmtDuration(duration)}
</div> </div>
{pbState?.updated && ( {isNewBest && (
<div style={{ padding: '10px 28px', background: 'rgba(245,197,24,0.12)', border: '1.5px solid rgba(245,197,24,0.5)', borderRadius: 10, fontSize: 15, fontWeight: 700, color: '#b8860b', letterSpacing: '0.04em' }}> <div style={{ padding: '10px 28px', background: 'rgba(245,197,24,0.12)', border: '1.5px solid rgba(245,197,24,0.5)', borderRadius: 10, fontSize: 15, fontWeight: 700, color: '#b8860b', letterSpacing: '0.04em', boxShadow: '0 0 12px rgba(245,197,24,0.45)' }}>
New Personal Best! 🏆 New Personal Best!
</div> </div>
)} )}
<button onClick={onDashboard} style={{ padding: '13px 40px', background: '#1a73e8', color: '#fff', border: 'none', borderRadius: 999, fontSize: 16, fontWeight: 600, cursor: 'pointer', fontFamily: 'system-ui,sans-serif' }}> <button onClick={onDashboard} style={{ padding: '13px 40px', background: '#1a73e8', color: '#fff', border: 'none', borderRadius: 999, fontSize: 16, fontWeight: 600, cursor: 'pointer', fontFamily: 'system-ui,sans-serif' }}>
@@ -311,6 +305,7 @@ export default function Game() {
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
const [elapsed, setElapsed] = useState(0); const [elapsed, setElapsed] = useState(0);
const [fetchError, setFetchError] = useState(false); const [fetchError, setFetchError] = useState(false);
const [isNewBest, setIsNewBest] = useState(false);
const timerStartRef = useRef(null); const timerStartRef = useRef(null);
const intervalRef = useRef(null); const intervalRef = useRef(null);
@@ -418,13 +413,35 @@ export default function Game() {
setPhase('question'); setPhase('question');
} }
function handleResultNext() { function handleQuit() {
if (!window.confirm('Are you sure you want to quit? Your progress will be lost.')) return;
stopTimer();
navigate('/dashboard');
}
async function handleResultNext() {
if (stage === 1) { if (stage === 1) {
s1CorrectRef.current = STAGE_SIZE - wrongCount; s1CorrectRef.current = STAGE_SIZE - wrongCount;
loadStage(2); loadStage(2);
} else { } else {
endSession(s1CorrectRef.current + (STAGE_SIZE - wrongCount)); endSession(s1CorrectRef.current + (STAGE_SIZE - wrongCount));
setDuration(stopTimer()); const token = sessionStorage.getItem('ranked_token');
const timeMs = timerStartRef.current ? Date.now() - timerStartRef.current : 0;
stopTimer();
let isNewBest = false;
try {
const res = await fetch(`${API}/me/best-time`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ grade, timeMs }),
});
const data = await res.json();
isNewBest = data.updated === true;
} catch (e) {
console.error('Failed to record best time:', e);
}
setDuration(timeMs);
setIsNewBest(isNewBest);
setPhase('complete'); setPhase('complete');
} }
} }
@@ -441,9 +458,9 @@ export default function Game() {
</div> </div>
); );
if (phase === 'intro') return <StageIntro stage={stage} onNext={() => { if (stage === 1) { startTimer(); startSession(); } setPhase('question'); }} />; if (phase === 'intro') return <StageIntro stage={stage} onNext={() => { if (stage === 1) { startTimer(); startSession(); } setPhase('question'); }} />;
if (phase === 'question') return <QuestionScreen question={questions[qIdx]} qIdx={qIdx} answers={answers} selected={selected} onSelect={setSelected} onNext={handleNext} onBack={handleBack} onJump={handleJump} />; if (phase === 'question') return <QuestionScreen question={questions[qIdx]} qIdx={qIdx} answers={answers} selected={selected} onSelect={setSelected} onNext={handleNext} onBack={handleBack} onJump={handleJump} onQuit={handleQuit} />;
if (phase === 'result') return <StageResult stage={stage} wrongCount={wrongCount} questions={questions} answers={answers} onRetry={handleRetry} onNext={handleResultNext} />; if (phase === 'result') return <StageResult stage={stage} wrongCount={wrongCount} questions={questions} answers={answers} onRetry={handleRetry} onNext={handleResultNext} />;
if (phase === 'complete') return <CompleteScreen duration={duration} grade={grade} onDashboard={() => navigate('/dashboard')} />; if (phase === 'complete') return <CompleteScreen duration={duration} isNewBest={isNewBest} onDashboard={() => navigate('/dashboard')} />;
return null; return null;
} }
+49 -382
View File
@@ -2,31 +2,14 @@ import { useState, useEffect, useRef } from 'react';
import { useNavigate, useLocation, Navigate } from 'react-router-dom'; import { useNavigate, useLocation, Navigate } from 'react-router-dom';
const VALID_GRADES = new Set(['G3', 'G6', 'G9']); const VALID_GRADES = new Set(['G3', 'G6', 'G9']);
const STAGE_SIZE = 11;
const API = import.meta.env.VITE_API_URL; const API = import.meta.env.VITE_API_URL;
const NAV_BTN = {
padding: '14px 40px', borderRadius: 999,
fontFamily: 'system-ui,sans-serif', fontSize: 17, fontWeight: 600,
cursor: 'pointer', border: '1.5px solid #b6d8f5',
background: '#e8f4fd', color: '#1a73e8',
};
function OpponentBar({ progress, total, name }) { function authHeaders() {
const pct = (progress / total) * 100; const token = sessionStorage.getItem('ranked_token');
return ( return token ? { Authorization: `Bearer ${token}` } : {};
<div style={{ marginTop: 8, padding: '8px 12px', background: '#fef9f0', borderRadius: 8, border: '1px solid #fce4c8', display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 13, fontWeight: 700, color: '#e67e22', whiteSpace: 'nowrap', fontFamily: 'system-ui,sans-serif', letterSpacing: '0.02em' }}>{name}</span>
<div style={{ flex: 1, height: 8, borderRadius: 4, background: '#f5e6d3', overflow: 'hidden' }}>
<div style={{ width: `${pct}%`, height: '100%', background: 'linear-gradient(90deg, #f39c12, #e67e22)', borderRadius: 4, transition: 'width 0.6s ease' }} />
</div>
<span style={{ fontSize: 13, fontWeight: 700, color: '#e67e22', minWidth: 36, textAlign: 'right', fontFamily: 'system-ui,sans-serif' }}>{Math.min(Math.round(progress), total)}/{total}</span>
</div>
);
} }
function FindingScreen({ elapsed, onBack, onForceMatch, showForceMatch }) { function FindingScreen({ elapsed, onBack }) {
const [dots, setDots] = useState(''); const [dots, setDots] = useState('');
useEffect(() => { const t = setInterval(() => setDots(d => d.length >= 3 ? '' : d + '.'), 400); return () => clearInterval(t); }, []); useEffect(() => { const t = setInterval(() => setDots(d => d.length >= 3 ? '' : d + '.'), 400); return () => clearInterval(t); }, []);
const minutes = Math.floor(elapsed / 60); const minutes = Math.floor(elapsed / 60);
@@ -40,165 +23,11 @@ function FindingScreen({ elapsed, onBack, onForceMatch, showForceMatch }) {
Time elapsed: {minutes}:{String(seconds).padStart(2, '0')} Time elapsed: {minutes}:{String(seconds).padStart(2, '0')}
</div> </div>
<button onClick={onBack} style={{ marginTop: 8, padding: '10px 32px', fontSize: 15, fontWeight: 600, background: '#f5f5f5', color: '#555', border: '1px solid #ccc', borderRadius: 999, cursor: 'pointer', fontFamily: 'system-ui,sans-serif' }}> Back to Dashboard</button> <button onClick={onBack} style={{ marginTop: 8, padding: '10px 32px', fontSize: 15, fontWeight: 600, background: '#f5f5f5', color: '#555', border: '1px solid #ccc', borderRadius: 999, cursor: 'pointer', fontFamily: 'system-ui,sans-serif' }}> Back to Dashboard</button>
{showForceMatch && (
<button onClick={onForceMatch} style={{ padding: '14px 36px', fontSize: 16, fontWeight: 700, background: '#ff69b4', color: '#000', border: 'none', borderRadius: 999, cursor: 'pointer', fontFamily: 'system-ui,sans-serif', boxShadow: '0 0 8px #ff69b4aa' }}> Force Match</button>
)}
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style> <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</div> </div>
); );
} }
function PlayerChip({ name, avatar, reverse }) {
const pic = avatar
? <img src={avatar} alt="" style={{ width: 56, height: 56, borderRadius: '50%', flexShrink: 0 }} referrerPolicy="no-referrer" />
: <div style={{ width: 56, height: 56, borderRadius: '50%', background: '#1a73e8', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22, fontWeight: 700, flexShrink: 0 }}>{name?.[0]?.toUpperCase() ?? '?'}</div>;
const label = <span style={{ fontSize: 17, fontWeight: 700, color: '#222', fontFamily: 'system-ui,sans-serif' }}>{name}</span>;
return (
<div style={{ display: 'flex', flexDirection: reverse ? 'row-reverse' : 'row', alignItems: 'center', gap: 12 }}>
{pic}{label}
</div>
);
}
function StageIntro({ grade, myName, myAvatar, opponentName, opponentAvatar, onNext }) {
const [count, setCount] = useState(5);
const gradeNum = grade?.replace('G', '') ?? '';
useEffect(() => {
const id = setInterval(() => {
setCount(c => {
if (c <= 1) { clearInterval(id); onNext(); return 0; }
return c - 1;
});
}, 1000);
return () => clearInterval(id);
}, [onNext]);
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', background: '#fff', fontFamily: 'system-ui,sans-serif', gap: 28 }}>
<p style={{ fontSize: 11, color: '#999', letterSpacing: 3, textTransform: 'uppercase', margin: 0 }}>Ranked Match</p>
<h1 style={{ fontSize: 'clamp(30px,5vw,52px)', fontWeight: 800, color: '#111', margin: 0, textAlign: 'center' }}>EQAO Grade {gradeNum}</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 20, flexWrap: 'wrap', justifyContent: 'center' }}>
<PlayerChip name={myName} avatar={myAvatar} reverse={false} />
<span style={{ fontSize: 18, fontWeight: 700, color: '#aaa', letterSpacing: 2 }}>vs</span>
<PlayerChip name={opponentName} avatar={opponentAvatar} reverse={true} />
</div>
<p style={{ fontSize: 14, color: '#aaa', margin: 0 }}>First to finish all {STAGE_SIZE} questions wins.</p>
<div style={{ fontSize: 'clamp(88px,18vw,144px)', fontWeight: 900, color: '#e53935', lineHeight: 1, fontVariantNumeric: 'tabular-nums', minWidth: 120, textAlign: 'center' }}>{count}</div>
</div>
);
}
function ProgressStrip({ total, current, answers, onJump }) {
const items = [];
for (let qi = 0; qi < total; qi++) {
const active = qi === current, answered = answers[qi] != null, filled = answered;
items.push(
<button key={`b-${qi}`} onClick={() => onJump && onJump(qi)} style={{ width: 36, height: 36, borderRadius: '50%', border: `2px solid ${filled || active ? '#1a73e8' : '#ccc'}`, background: filled ? '#1a73e8' : active ? '#e8f0fe' : '#fff', color: filled ? '#fff' : active ? '#1a73e8' : '#aaa', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, fontFamily: 'system-ui,-apple-system,sans-serif', flexShrink: 0, cursor: 'pointer', padding: 0, boxSizing: 'border-box', transition: 'background 0.15s, border-color 0.15s' }}>{qi + 1}</button>
);
if (qi < total - 1) items.push(<div key={`l-${qi}`} style={{ flex: 1, height: 3, minWidth: 2, background: answers[qi] != null ? '#1a73e8' : '#ddd' }} />);
}
return <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'nowrap', width: '100%' }}>{items}</div>;
}
function QuestionScreen({ question: q, qIdx, answers, selected, onSelect, onNext, onBack, onJump, opponentProgress, opponentName }) {
useEffect(() => {
const handler = (e) => {
const i = +e.key - 1;
if (i >= 0 && i < q.choices.length) { onSelect(i); return; }
if ((e.key === 'Enter' || e.key === 'ArrowRight') && qIdx < STAGE_SIZE - 1) onNext();
if (e.key === 'ArrowLeft' && qIdx > 0) onBack();
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [q.choices.length, qIdx, onSelect, onNext, onBack]);
return (
<div style={{ background: '#cce8f4', minHeight: '100vh', fontFamily: 'system-ui,-apple-system,sans-serif' }}>
<div style={{ maxWidth: 760, margin: '0 auto', background: '#fff', minHeight: '100vh', display: 'flex', flexDirection: 'column', boxShadow: '0 0 40px rgba(0,60,120,0.10)' }}>
<div style={{ borderBottom: '1px solid #e8e8e8', padding: '20px 32px 16px' }}>
<p style={{ fontSize: 36, fontWeight: 700, color: '#333', margin: '0 0 14px 0', lineHeight: 1 }}>Question {qIdx + 1} of {STAGE_SIZE}</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: '#1a73e8', textTransform: 'uppercase', letterSpacing: '0.05em', fontFamily: 'system-ui,sans-serif' }}>You</span>
<div style={{ flex: 1 }}><ProgressStrip total={STAGE_SIZE} current={qIdx} answers={answers} onJump={onJump} /></div>
</div>
<OpponentBar progress={opponentProgress} total={STAGE_SIZE} name={opponentName} />
</div>
<div style={{ flex: 1, padding: '28px 32px 24px' }}>
<p style={{ fontSize: 24, color: '#111', lineHeight: 1.7, whiteSpace: 'pre-line', fontWeight: 500, margin: '0 0 28px 0' }}>{q.question}</p>
<div style={q.layout === 'grid' ? { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } : { display: 'flex', flexDirection: 'column', gap: 12 }}>
{q.choices.map((choice, i) => {
const sel = selected === i;
return (
<button key={i} onClick={() => onSelect(i)} style={{ minHeight: 64, padding: '18px 24px', background: sel ? '#e8f0fe' : '#f7f8fa', border: `2px solid ${sel ? '#1a73e8' : '#e0e0e0'}`, borderRadius: 12, fontSize: 20, color: sel ? '#1a73e8' : '#222', textAlign: 'left', cursor: 'pointer', fontWeight: sel ? 600 : 400, fontFamily: 'system-ui,-apple-system,sans-serif', lineHeight: 1.45, transition: 'background 0.12s, border-color 0.12s' }}>
<span style={{ fontSize: 14, fontWeight: 700, color: sel ? '#1a73e8' : '#aaa', marginRight: 20, opacity: 0.7 }}>{i + 1}</span>{choice}
</button>
);
})}
</div>
</div>
<div style={{ borderTop: '1px solid #e0e0e0', boxShadow: '0 -2px 8px rgba(0,0,0,0.06)', padding: '16px 32px', display: 'flex', justifyContent: 'flex-end', gap: 14 }}>
{qIdx > 0 && <button style={NAV_BTN} onClick={onBack}> Back</button>}
{qIdx === STAGE_SIZE - 1 ? (
<button style={{ padding: '14px 40px', borderRadius: 999, fontSize: 17, fontWeight: 700, cursor: 'pointer', border: 'none', fontFamily: 'system-ui,sans-serif', background: '#ff69b4', color: '#000', boxShadow: '0 0 6px #ff69b4aa' }} onClick={onNext}>Submit </button>
) : (
<button style={NAV_BTN} onClick={onNext}>Next </button>
)}
</div>
</div>
</div>
);
}
function DefeatOverlay({ opponentName, onViewResults }) {
return (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 2000 }}>
<div style={{ background: '#fff', borderRadius: 16, padding: '40px 48px', textAlign: 'center', maxWidth: 380, width: '90%', fontFamily: 'system-ui,sans-serif', boxShadow: '0 8px 32px rgba(0,0,0,0.2)' }}>
<div style={{ fontSize: 64, marginBottom: 12 }}>💀</div>
<h1 style={{ fontSize: 32, fontWeight: 800, color: '#c62828', margin: '0 0 8px' }}>Defeated!</h1>
<p style={{ fontSize: 16, color: '#555', margin: '0 0 24px', lineHeight: 1.5 }}>{opponentName} finished before you.</p>
<button onClick={onViewResults} style={{ padding: '12px 36px', fontSize: 16, fontWeight: 600, background: '#1a73e8', color: '#fff', border: 'none', borderRadius: 999, cursor: 'pointer', fontFamily: 'system-ui,sans-serif' }}>See Results</button>
</div>
</div>
);
}
function WaitingScreen({ opponentName, opponentProgress }) {
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', background: '#fff', fontFamily: 'system-ui,sans-serif', gap: 24 }}>
<div style={{ width: 48, height: 48, borderRadius: '50%', border: '3px solid #e0e0e0', borderTopColor: '#ff69b4', animation: 'spin 0.6s linear infinite' }} />
<h2 style={{ fontSize: 22, fontWeight: 600, color: '#333', margin: 0 }}>Waiting for {opponentName}...</h2>
<p style={{ fontSize: 15, color: '#888', margin: 0 }}>You finished! Now waiting for your opponent to complete.</p>
<div style={{ width: 300, marginTop: 8 }}>
<OpponentBar progress={opponentProgress} total={STAGE_SIZE} name={opponentName} />
</div>
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</div>
);
}
function CompleteScreen({ won, opponentName, duration, eloChange, onDashboard }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh', background: '#fff', fontFamily: 'system-ui,sans-serif' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px 32px', textAlign: 'center', gap: 20 }}>
<div style={{ fontSize: 72 }}>{won ? '🏆' : '💀'}</div>
<h1 style={{ fontSize: 36, fontWeight: 800, color: won ? '#1b8a2d' : '#c62828', margin: 0 }}>{won ? 'Victory!' : 'Defeated!'}</h1>
<p style={{ fontSize: 17, color: '#555', maxWidth: 400, margin: 0 }}>{won ? `You beat ${opponentName}! Well played.` : `${opponentName} beat you. Better luck next time.`}</p>
<div style={{ padding: '14px 40px', background: won ? '#f0faf0' : '#fef0f0', border: `2px solid ${won ? '#a5d6a7' : '#ef9a9a'}`, borderRadius: 12, fontSize: 24, fontWeight: 700, color: won ? '#1b8a2d' : '#c62828' }}>
{Math.floor(duration / 1000 / 60)}:{String(Math.floor(duration / 1000) % 60).padStart(2, '0')}
</div>
{eloChange !== 0 && <div style={{ padding: '10px 24px', borderRadius: 8, fontSize: 18, fontWeight: 700, fontFamily: 'system-ui,sans-serif', color: eloChange > 0 ? '#1b8a2d' : '#c62828' }}>ELO {eloChange > 0 ? '+' : ''}{eloChange}</div>}
<button onClick={onDashboard} style={{ padding: '13px 40px', background: '#1a73e8', color: '#fff', border: 'none', borderRadius: 999, fontSize: 16, fontWeight: 600, cursor: 'pointer', fontFamily: 'system-ui,sans-serif' }}>Back to Dashboard</button>
</div>
</div>
);
}
function authHeaders() {
const token = sessionStorage.getItem('ranked_token');
return token ? { Authorization: `Bearer ${token}` } : {};
}
export default function Queue() { export default function Queue() {
const navigate = useNavigate(); const navigate = useNavigate();
const { state } = useLocation(); const { state } = useLocation();
@@ -206,73 +35,19 @@ export default function Queue() {
const [myName, setMyName] = useState(''); const [myName, setMyName] = useState('');
const [myAvatar, setMyAvatar] = useState(null); const [myAvatar, setMyAvatar] = useState(null);
const [phase, setPhase] = useState('finding');
const [questions, setQuestions] = useState([]);
const [qIdx, setQIdx] = useState(0);
const [answers, setAnswers] = useState({});
const [selected, setSelected] = useState(null);
const [opponentProgress, setOpponentProgress] = useState(0);
const [opponentName, setOpponentName] = useState('');
const [opponentAvatar, setOpponentAvatar] = useState(null);
const [playerWon, setPlayerWon] = useState(null);
const [showDefeatOverlay, setShowDefeatOverlay] = useState(false);
const [startTime] = useState(() => Date.now());
const [duration, setDuration] = useState(0);
const [eloChange, setEloChange] = useState(0);
const [queueElapsed, setQueueElapsed] = useState(0); const [queueElapsed, setQueueElapsed] = useState(0);
const [showForceMatch, setShowForceMatch] = useState(false);
const gameIdRef = useRef(null);
const pollRef = useRef(null); const pollRef = useRef(null);
const completedRef = useRef(false);
const submittedRef = useRef(false);
const joinedRef = useRef(false); const joinedRef = useRef(false);
const completedRef = useRef(false);
const leaveTimerRef = useRef(null); const leaveTimerRef = useRef(null);
const leavingRef = useRef(false);
// Elapsed timer while finding // Elapsed timer
useEffect(() => { useEffect(() => {
if (phase !== 'finding') return;
const id = setInterval(() => setQueueElapsed(e => e + 1), 1000); const id = setInterval(() => setQueueElapsed(e => e + 1), 1000);
return () => clearInterval(id); return () => clearInterval(id);
}, [phase]); }, []);
// Show force match after 15s
useEffect(() => {
if (phase !== 'finding') return;
const id = setTimeout(() => setShowForceMatch(true), 15000);
return () => clearTimeout(id);
}, [phase]);
async function forceMatch() {
clearInterval(pollRef.current);
console.log('[queue] force match requested');
try {
const r = await fetch(`${API}/queue/debug`, { headers: authHeaders() });
const debug = await r.json();
console.log('[queue] debug state:', JSON.stringify(debug));
} catch {}
fetch(`${API}/queue/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({ grade }),
})
.then(r => {
if (r.status === 401) { sessionStorage.removeItem('ranked_token'); navigate('/'); return null; }
return r.json();
})
.then(data => {
console.log('[queue] force match result:', JSON.stringify(data));
if (!data) return;
joinedRef.current = true;
if (data.status === 'waiting') {
pollQueue();
} else if (data.status === 'matched') {
fetchMatchData(data.gameId);
}
})
.catch(() => {});
}
// Fetch own user info // Fetch own user info
useEffect(() => { useEffect(() => {
@@ -288,43 +63,43 @@ export default function Queue() {
// Join queue on mount // Join queue on mount
useEffect(() => { useEffect(() => {
// Cancel any pending leave from a previous unmount (Strict Mode guard) // Cancel any pending leave from a previous mount (handles StrictMode double-invoke)
if (leaveTimerRef.current) { if (leaveTimerRef.current) {
clearTimeout(leaveTimerRef.current); clearTimeout(leaveTimerRef.current);
leaveTimerRef.current = null; leaveTimerRef.current = null;
} }
// Reset leaving flag so callbacks work on this mount
leavingRef.current = false;
joinedRef.current = false;
completedRef.current = false;
let cancelled = false;
console.log('[queue] joining with grade', grade);
fetch(`${API}/queue/join`, { fetch(`${API}/queue/join`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() }, headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({ grade }), body: JSON.stringify({ grade }),
}) })
.then(r => { .then(r => {
if (r.status === 401) { console.log('[queue] 401 on join'); sessionStorage.removeItem('ranked_token'); navigate('/'); return null; } if (r.status === 401) { sessionStorage.removeItem('ranked_token'); navigate('/'); return null; }
return r.json(); return r.json();
}) })
.then(data => { .then(data => {
console.log('[queue] join response:', JSON.stringify(data)); if (!data || leavingRef.current) return;
if (!data || cancelled) return;
joinedRef.current = true; joinedRef.current = true;
if (data.status === 'waiting') { if (data.status === 'waiting') {
pollQueue(); pollQueue();
} else if (data.status === 'matched') { } else if (data.status === 'matched') {
fetchMatchData(data.gameId); fetchMatchData();
} }
}) })
.catch(e => console.log('[queue] join error:', e)); .catch(() => {});
return () => { return () => {
cancelled = true; leavingRef.current = true;
clearInterval(pollRef.current); clearInterval(pollRef.current);
if (joinedRef.current && !gameIdRef.current && !completedRef.current) { if (joinedRef.current && !completedRef.current) {
leaveTimerRef.current = setTimeout(() => { leaveTimerRef.current = setTimeout(() => {
fetch(`${API}/queue/leave`, { method: 'DELETE', headers: authHeaders() }).catch(() => {}); fetch(`${API}/queue/leave`, { method: 'DELETE', headers: authHeaders() }).catch(() => {});
}, 300); }, 0);
} }
}; };
}, [grade, navigate]); }, [grade, navigate]);
@@ -333,21 +108,15 @@ export default function Queue() {
pollRef.current = setInterval(() => { pollRef.current = setInterval(() => {
fetch(`${API}/queue/status`, { headers: authHeaders() }) fetch(`${API}/queue/status`, { headers: authHeaders() })
.then(r => { .then(r => {
if (r.status === 401) { console.log('[queue] 401 on poll'); sessionStorage.removeItem('ranked_token'); navigate('/'); return null; } if (r.status === 401) { sessionStorage.removeItem('ranked_token'); navigate('/'); return null; }
return r.json(); return r.json();
}) })
.then(data => { .then(data => {
if (!data) return; if (!data || leavingRef.current) return;
if (data.status === 'matched') { if (data.status === 'matched') {
console.log('[queue] poll found match!');
clearInterval(pollRef.current); clearInterval(pollRef.current);
gameIdRef.current = data.gameId; navigateToGame(data);
setQuestions(data.questions);
setOpponentName(data.opponent.username ?? data.opponent.displayName);
setOpponentAvatar(data.opponent.avatarUrl);
setPhase('intro');
} else if (data.status === 'not_in_queue') { } else if (data.status === 'not_in_queue') {
console.log('[queue] poll got not_in_queue, rejoining');
clearInterval(pollRef.current); clearInterval(pollRef.current);
fetch(`${API}/queue/join`, { fetch(`${API}/queue/join`, {
method: 'POST', method: 'POST',
@@ -355,151 +124,49 @@ export default function Queue() {
body: JSON.stringify({ grade }), body: JSON.stringify({ grade }),
}) })
.then(r => { .then(r => {
if (r.status === 401) { console.log('[queue] 401 on rejoin'); sessionStorage.removeItem('ranked_token'); navigate('/'); return null; } if (r.status === 401) { sessionStorage.removeItem('ranked_token'); navigate('/'); return null; }
return r.json(); return r.json();
}) })
.then(d => { .then(d => {
console.log('[queue] rejoin response:', JSON.stringify(d)); if (!d || leavingRef.current) return;
if (!d) return;
if (d.status === 'waiting') pollQueue(); if (d.status === 'waiting') pollQueue();
else if (d.status === 'matched') fetchMatchData(d.gameId); else if (d.status === 'matched') navigateToGame(d);
}) })
.catch(() => {}); .catch(() => {});
} }
}) })
.catch(() => {}); .catch(() => {});
}, 1500);
}
function fetchMatchData(gameId) {
gameIdRef.current = gameId;
fetch(`${API}/queue/status`, { headers: authHeaders() })
.then(r => r.json())
.then(data => {
if (data.status === 'matched') {
setQuestions(data.questions);
setOpponentName(data.opponent.username ?? data.opponent.displayName);
setOpponentAvatar(data.opponent.avatarUrl);
setPhase('intro');
}
})
.catch(() => {});
}
function submitProgress(currentQuestion, correctAnswers, finished, timeSpentMs) {
fetch(`${API}/game/progress`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({ gameId: gameIdRef.current, currentQuestion, correctAnswers, finished, timeSpentMs }),
}).catch(() => {});
}
function pollGameStatus() {
const id = gameIdRef.current;
if (!id) return;
pollRef.current = setInterval(() => {
if (completedRef.current) { clearInterval(pollRef.current); return; }
fetch(`${API}/game/status/${id}`, { headers: authHeaders() })
.then(r => r.json())
.then(data => {
if (data.status === 'completed') {
clearInterval(pollRef.current);
const you = data.players.you;
const won = data.youWon === true;
completedRef.current = true;
setDuration(you.timeSpentMs || (Date.now() - startTime));
setEloChange(you.eloChange || 0);
setPlayerWon(won);
if (won) {
setPhase('complete');
} else {
setShowDefeatOverlay(true);
}
} else {
const opp = data.players.opponent;
if (opp.username) setOpponentName(opp.username);
setOpponentProgress(opp.currentQuestion + 1);
}
})
.catch(() => {});
}, 1000); }, 1000);
} }
function handleNext() { function fetchMatchData() {
const newAnswers = { ...answers, [qIdx]: selected }; fetch(`${API}/queue/status`, { headers: authHeaders() })
setAnswers(newAnswers); .then(r => r.json())
.then(data => {
const correctCount = questions.filter((q, i) => newAnswers[i] === q.correct).length; if (!leavingRef.current && data.status === 'matched') {
const elapsed = Date.now() - startTime; navigateToGame(data);
if (qIdx < STAGE_SIZE - 1) {
submitProgress(qIdx + 1, correctCount, false);
const next = qIdx + 1;
setQIdx(next);
setSelected(newAnswers[next] ?? null);
} else {
submittedRef.current = true;
submitProgress(STAGE_SIZE, correctCount, true, elapsed);
setPhase('waiting');
} }
})
.catch(() => {});
} }
function handleBack() { function navigateToGame(data) {
const newAnswers = selected !== null ? { ...answers, [qIdx]: selected } : answers; clearInterval(pollRef.current);
setAnswers(newAnswers); completedRef.current = true;
const prev = qIdx - 1; navigate('/ranked-game', {
setQIdx(prev); state: {
setSelected(newAnswers[prev] ?? null); gameId: data.gameId,
} questions: data.questions,
opponentName: data.opponent.username ?? data.opponent.displayName,
function handleJump(targetIdx) { opponentAvatar: data.opponent.avatarUrl,
const newAnswers = selected !== null ? { ...answers, [qIdx]: selected } : answers; myName,
setAnswers(newAnswers); myAvatar,
setQIdx(targetIdx); grade,
setSelected(newAnswers[targetIdx] ?? null); },
});
} }
if (!VALID_GRADES.has(grade)) return <Navigate to="/dashboard" replace />; if (!VALID_GRADES.has(grade)) return <Navigate to="/dashboard" replace />;
if (phase === 'finding') return <FindingScreen elapsed={queueElapsed} onBack={() => navigate('/dashboard')} />;
return <FindingScreen elapsed={queueElapsed} onBack={() => navigate('/dashboard')} showForceMatch={showForceMatch} onForceMatch={forceMatch} />;
if (phase === 'intro')
return <StageIntro grade={grade} myName={myName} myAvatar={myAvatar} opponentName={opponentName} opponentAvatar={opponentAvatar} onNext={() => { setPhase('question'); pollGameStatus(); }} />;
if (phase === 'question')
return (
<>
<QuestionScreen
question={questions[qIdx]} qIdx={qIdx} answers={answers} selected={selected}
onSelect={setSelected} onNext={handleNext} onBack={handleBack} onJump={handleJump}
opponentProgress={opponentProgress} opponentName={opponentName}
/>
{showDefeatOverlay && (
<DefeatOverlay opponentName={opponentName} onViewResults={() => { setShowDefeatOverlay(false); setPhase('complete'); }} />
)}
</>
);
if (phase === 'waiting')
return (
<>
<WaitingScreen opponentName={opponentName} opponentProgress={opponentProgress} />
{showDefeatOverlay && (
<DefeatOverlay opponentName={opponentName} onViewResults={() => { setShowDefeatOverlay(false); setPhase('complete'); }} />
)}
</>
);
if (phase === 'complete')
return (
<CompleteScreen
won={playerWon} opponentName={opponentName} duration={duration}
eloChange={eloChange} onDashboard={() => navigate('/dashboard')}
/>
);
return null;
} }
+381
View File
@@ -0,0 +1,381 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate, useLocation, Navigate } from 'react-router-dom';
const STAGE_SIZE = 11;
const API = import.meta.env.VITE_API_URL;
const NAV_BTN = {
padding: '14px 40px', borderRadius: 999,
fontFamily: 'system-ui,sans-serif', fontSize: 17, fontWeight: 600,
cursor: 'pointer', border: '1.5px solid #b6d8f5',
background: '#e8f4fd', color: '#1a73e8',
};
const FORFEIT_BTN = {
padding: '14px 28px', borderRadius: 999, fontFamily: 'system-ui,sans-serif',
fontSize: 14, fontWeight: 600, cursor: 'pointer',
border: '1.5px solid #e57373', background: '#fff', color: '#c62828',
};
function authHeaders() {
const token = sessionStorage.getItem('ranked_token');
return token ? { Authorization: `Bearer ${token}` } : {};
}
function OpponentProgressStrip({ progress, total, name, dced, disconnected }) {
const items = [];
for (let qi = 0; qi < total; qi++) {
const filled = qi < progress;
items.push(
<div key={`b-${qi}`} style={{ width: 30, height: 30, borderRadius: '50%', border: `2px solid ${filled ? '#e67e22' : '#ccc'}`, background: filled ? '#e67e22' : '#fff', color: filled ? '#fff' : '#aaa', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, fontFamily: 'system-ui,-apple-system,sans-serif', flexShrink: 0, padding: 0, boxSizing: 'border-box', transition: 'background 0.15s, border-color 0.15s' }}>{qi + 1}</div>
);
if (qi < total - 1) {
items.push(
<div key={`l-${qi}`} style={{ flex: 1, height: 2, minWidth: 2, background: filled ? '#e67e22' : '#ddd' }} />
);
}
}
return (
<div style={{ marginTop: 8, padding: '10px 0', display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: (dced || disconnected) ? '#c62828' : '#e67e22', textTransform: 'uppercase', letterSpacing: '0.05em', fontFamily: 'system-ui,sans-serif' }}>{name}</span>
{dced && <span style={{ fontSize: 10, fontWeight: 700, color: '#fff', background: '#c62828', borderRadius: 4, padding: '1px 5px', fontFamily: 'system-ui,sans-serif', letterSpacing: '0.04em' }}>DC'd</span>}
{!dced && disconnected && <span style={{ fontSize: 10, fontWeight: 700, color: '#fff', background: '#e57373', borderRadius: 4, padding: '1px 5px', fontFamily: 'system-ui,sans-serif', letterSpacing: '0.04em' }}>disconnected?</span>}
</div>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'nowrap', width: '100%', opacity: (dced || disconnected) ? 0.5 : 1 }}>
{items}
</div>
</div>
);
}
function ProgressStrip({ total, current, answers, onJump }) {
const items = [];
for (let qi = 0; qi < total; qi++) {
const active = qi === current, answered = answers[qi] != null, filled = answered;
items.push(
<button key={`b-${qi}`} onClick={() => onJump && onJump(qi)} style={{ width: 36, height: 36, borderRadius: '50%', border: `2px solid ${filled || active ? '#1a73e8' : '#ccc'}`, background: filled ? '#1a73e8' : active ? '#e8f0fe' : '#fff', color: filled ? '#fff' : active ? '#1a73e8' : '#aaa', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, fontFamily: 'system-ui,-apple-system,sans-serif', flexShrink: 0, cursor: 'pointer', padding: 0, boxSizing: 'border-box', transition: 'background 0.15s, border-color 0.15s' }}>{qi + 1}</button>
);
if (qi < total - 1) items.push(<div key={`l-${qi}`} style={{ flex: 1, height: 3, minWidth: 2, background: answers[qi] != null ? '#1a73e8' : '#ddd' }} />);
}
return <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'nowrap', width: '100%' }}>{items}</div>;
}
function PlayerChip({ name, avatar, reverse }) {
const pic = avatar
? <img src={avatar} alt="" style={{ width: 56, height: 56, borderRadius: '50%', flexShrink: 0 }} referrerPolicy="no-referrer" />
: <div style={{ width: 56, height: 56, borderRadius: '50%', background: '#1a73e8', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22, fontWeight: 700, flexShrink: 0 }}>{name?.[0]?.toUpperCase() ?? '?'}</div>;
const label = <span style={{ fontSize: 17, fontWeight: 700, color: '#222', fontFamily: 'system-ui,sans-serif' }}>{name}</span>;
return (
<div style={{ display: 'flex', flexDirection: reverse ? 'row-reverse' : 'row', alignItems: 'center', gap: 12 }}>
{pic}{label}
</div>
);
}
function StageIntro({ grade, myName, myAvatar, opponentName, opponentAvatar, onNext }) {
const [count, setCount] = useState(5);
const gradeNum = grade?.replace('G', '') ?? '';
useEffect(() => {
const id = setInterval(() => {
setCount(c => {
if (c <= 1) { clearInterval(id); onNext(); return 0; }
return c - 1;
});
}, 1000);
return () => clearInterval(id);
}, [onNext]);
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', background: '#fff', fontFamily: 'system-ui,sans-serif', gap: 28 }}>
<p style={{ fontSize: 11, color: '#999', letterSpacing: 3, textTransform: 'uppercase', margin: 0 }}>Ranked Match</p>
<h1 style={{ fontSize: 'clamp(30px,5vw,52px)', fontWeight: 800, color: '#111', margin: 0, textAlign: 'center' }}>EQAO Grade {gradeNum}</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 20, flexWrap: 'wrap', justifyContent: 'center' }}>
<PlayerChip name={myName} avatar={myAvatar} reverse={false} />
<span style={{ fontSize: 18, fontWeight: 700, color: '#aaa', letterSpacing: 2 }}>vs</span>
<PlayerChip name={opponentName} avatar={opponentAvatar} reverse={true} />
</div>
<p style={{ fontSize: 14, color: '#aaa', margin: 0 }}>First to finish all {STAGE_SIZE} questions wins.</p>
<div style={{ fontSize: 'clamp(88px,18vw,144px)', fontWeight: 900, color: '#e53935', lineHeight: 1, fontVariantNumeric: 'tabular-nums', minWidth: 120, textAlign: 'center' }}>{count}</div>
</div>
);
}
function QuestionScreen({ question: q, qIdx, answers, selected, onSelect, onNext, onBack, onJump, opponentProgress, opponentName, opponentDced, opponentConnected, onForfeit }) {
useEffect(() => {
const handler = (e) => {
const i = +e.key - 1;
if (i >= 0 && i < q.choices.length) { onSelect(i); return; }
if ((e.key === 'Enter' || e.key === 'ArrowRight') && qIdx < STAGE_SIZE - 1) onNext();
if (e.key === 'ArrowLeft' && qIdx > 0) onBack();
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [q.choices.length, qIdx, onSelect, onNext, onBack]);
return (
<div style={{ background: '#cce8f4', minHeight: '100vh', fontFamily: 'system-ui,-apple-system,sans-serif' }}>
<div style={{ maxWidth: 760, margin: '0 auto', background: '#fff', minHeight: '100vh', display: 'flex', flexDirection: 'column', boxShadow: '0 0 40px rgba(0,60,120,0.10)' }}>
<div style={{ borderBottom: '1px solid #e8e8e8', padding: '20px 32px 16px' }}>
<p style={{ fontSize: 36, fontWeight: 700, color: '#333', margin: '0 0 14px 0', lineHeight: 1 }}>Question {qIdx + 1} of {STAGE_SIZE}</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: '#1a73e8', textTransform: 'uppercase', letterSpacing: '0.05em', fontFamily: 'system-ui,sans-serif' }}>You</span>
<div style={{ flex: 1 }}><ProgressStrip total={STAGE_SIZE} current={qIdx} answers={answers} onJump={onJump} /></div>
</div>
<OpponentProgressStrip progress={opponentProgress} total={STAGE_SIZE} name={opponentName} dced={opponentDced} disconnected={!opponentConnected && !opponentDced} />
</div>
<div style={{ flex: 1, padding: '28px 32px 24px' }}>
<p style={{ fontSize: 24, color: '#111', lineHeight: 1.7, whiteSpace: 'pre-line', fontWeight: 500, margin: '0 0 28px 0' }}>{q.question}</p>
<div style={q.layout === 'grid' ? { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } : { display: 'flex', flexDirection: 'column', gap: 12 }}>
{q.choices.map((choice, i) => {
const sel = selected === i;
return (
<button key={i} onClick={() => onSelect(i)} style={{ minHeight: 64, padding: '18px 24px', background: sel ? '#e8f0fe' : '#f7f8fa', border: `2px solid ${sel ? '#1a73e8' : '#e0e0e0'}`, borderRadius: 12, fontSize: 20, color: sel ? '#1a73e8' : '#222', textAlign: 'left', cursor: 'pointer', fontWeight: sel ? 600 : 400, fontFamily: 'system-ui,-apple-system,sans-serif', lineHeight: 1.45, transition: 'background 0.12s, border-color 0.12s' }}>
<span style={{ fontSize: 14, fontWeight: 700, color: sel ? '#1a73e8' : '#aaa', marginRight: 20, opacity: 0.7 }}>{i + 1}</span>{choice}
</button>
);
})}
</div>
</div>
<div style={{ borderTop: '1px solid #e0e0e0', boxShadow: '0 -2px 8px rgba(0,0,0,0.06)', padding: '16px 32px', display: 'flex', justifyContent: 'space-between', gap: 14 }}>
<button style={FORFEIT_BTN} onClick={onForfeit}>Forfeit</button>
<div style={{ display: 'flex', gap: 14 }}>
{qIdx > 0 && <button style={NAV_BTN} onClick={onBack}> Back</button>}
{qIdx === STAGE_SIZE - 1 ? (
<button style={{ padding: '14px 40px', borderRadius: 999, fontSize: 17, fontWeight: 700, cursor: 'pointer', border: 'none', fontFamily: 'system-ui,sans-serif', background: '#ff69b4', color: '#000', boxShadow: '0 0 6px #ff69b4aa' }} onClick={onNext}>Submit ★</button>
) : (
<button style={NAV_BTN} onClick={onNext}>Next </button>
)}
</div>
</div>
</div>
</div>
);
}
function DefeatOverlay({ opponentName, onViewResults }) {
return (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 2000 }}>
<div style={{ background: '#fff', borderRadius: 16, padding: '40px 48px', textAlign: 'center', maxWidth: 380, width: '90%', fontFamily: 'system-ui,sans-serif', boxShadow: '0 8px 32px rgba(0,0,0,0.2)' }}>
<div style={{ fontSize: 64, marginBottom: 12 }}>💀</div>
<h1 style={{ fontSize: 32, fontWeight: 800, color: '#c62828', margin: '0 0 8px' }}>Defeated!</h1>
<p style={{ fontSize: 16, color: '#555', margin: '0 0 24px', lineHeight: 1.5 }}>{opponentName} finished before you.</p>
<button onClick={onViewResults} style={{ padding: '12px 36px', fontSize: 16, fontWeight: 600, background: '#1a73e8', color: '#fff', border: 'none', borderRadius: 999, cursor: 'pointer', fontFamily: 'system-ui,sans-serif' }}>See Results</button>
</div>
</div>
);
}
function WaitingScreen({ opponentName, opponentProgress, opponentDced, opponentConnected, onForfeit }) {
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', background: '#fff', fontFamily: 'system-ui,sans-serif', gap: 24 }}>
{opponentDced
? <div style={{ fontSize: 48 }}>🔌</div>
: <div style={{ width: 48, height: 48, borderRadius: '50%', border: '3px solid #e0e0e0', borderTopColor: '#ff69b4', animation: 'spin 0.6s linear infinite' }} />
}
<h2 style={{ fontSize: 22, fontWeight: 600, color: opponentDced ? '#c62828' : '#333', margin: 0 }}>
{opponentDced ? `${opponentName} disconnected!` : `Waiting for ${opponentName}...`}
</h2>
<p style={{ fontSize: 15, color: '#888', margin: 0 }}>
{opponentDced ? 'Awarding victory...' : 'You finished! Now waiting for your opponent to complete.'}
</p>
<div style={{ width: '100%', maxWidth: 480, marginTop: 8 }}>
<OpponentProgressStrip progress={opponentProgress} total={STAGE_SIZE} name={opponentName} dced={opponentDced} disconnected={!opponentConnected && !opponentDced} />
</div>
<button onClick={onForfeit} style={{ marginTop: 16, ...FORFEIT_BTN }}>Forfeit & Leave</button>
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</div>
);
}
function CompleteScreen({ won, opponentName, duration, eloChange, opponentDced, youDced, onDashboard }) {
const subtitle = opponentDced && won
? `${opponentName} disconnected. Victory by default.`
: youDced && !won
? `You disconnected. ${opponentName} wins.`
: won
? `You beat ${opponentName}! Well played.`
: `${opponentName} beat you. Better luck next time.`;
return (
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh', background: '#fff', fontFamily: 'system-ui,sans-serif' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px 32px', textAlign: 'center', gap: 20 }}>
<div style={{ fontSize: 72 }}>{won ? (opponentDced ? '🔌' : '🏆') : '💀'}</div>
<h1 style={{ fontSize: 36, fontWeight: 800, color: won ? '#1b8a2d' : '#c62828', margin: 0 }}>{won ? 'Victory!' : 'Defeated!'}</h1>
<p style={{ fontSize: 17, color: '#555', maxWidth: 400, margin: 0 }}>{subtitle}</p>
{duration > 0 && (
<div style={{ padding: '14px 40px', background: won ? '#f0faf0' : '#fef0f0', border: `2px solid ${won ? '#a5d6a7' : '#ef9a9a'}`, borderRadius: 12, fontSize: 24, fontWeight: 700, color: won ? '#1b8a2d' : '#c62828' }}>
⏱ {Math.floor(duration / 1000 / 60)}:{String(Math.floor(duration / 1000) % 60).padStart(2, '0')}
</div>
)}
{eloChange !== 0 && <div style={{ padding: '10px 24px', borderRadius: 8, fontSize: 18, fontWeight: 700, fontFamily: 'system-ui,sans-serif', color: eloChange > 0 ? '#1b8a2d' : '#c62828' }}>ELO {eloChange > 0 ? '+' : ''}{eloChange}</div>}
<button onClick={onDashboard} style={{ padding: '13px 40px', background: '#1a73e8', color: '#fff', border: 'none', borderRadius: 999, fontSize: 16, fontWeight: 600, cursor: 'pointer', fontFamily: 'system-ui,sans-serif' }}>Back to Dashboard</button>
</div>
</div>
);
}
export default function RankedGame() {
const navigate = useNavigate();
const { state } = useLocation();
if (!state || !state.gameId || !state.questions) {
return <Navigate to="/dashboard" replace />;
}
const { gameId, questions: initialQuestions, opponentName: initOppName, opponentAvatar: initOppAvatar, myName: initMyName, myAvatar: initMyAvatar, grade } = state;
const [phase, setPhase] = useState('intro');
const [questions] = useState(initialQuestions);
const [qIdx, setQIdx] = useState(0);
const [answers, setAnswers] = useState({});
const [selected, setSelected] = useState(null);
const [opponentProgress, setOpponentProgress] = useState(0);
const [opponentName] = useState(initOppName);
const [opponentAvatar] = useState(initOppAvatar);
const [playerWon, setPlayerWon] = useState(null);
const [showDefeatOverlay, setShowDefeatOverlay] = useState(false);
const [startTime] = useState(() => Date.now());
const [duration, setDuration] = useState(0);
const [eloChange, setEloChange] = useState(0);
const [opponentDced, setOpponentDced] = useState(false);
const [youDced, setYouDced] = useState(false);
const [opponentConnected, setOpponentConnected] = useState(true);
const pollRef = useRef(null);
const completedRef = useRef(false);
const submittedRef = useRef(false);
useEffect(() => {
return () => clearInterval(pollRef.current);
}, []);
function submitProgress(currentQuestion, correctAnswers, finished, timeSpentMs) {
fetch(`${API}/game/progress`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({ gameId, currentQuestion, correctAnswers, finished, timeSpentMs }),
}).catch(() => {});
}
function pollGameStatus() {
pollRef.current = setInterval(() => {
if (completedRef.current) { clearInterval(pollRef.current); return; }
fetch(`${API}/game/status/${gameId}`, { headers: authHeaders() })
.then(r => r.json())
.then(data => {
if (data.opponentDced) setOpponentDced(true);
if (data.youDced) setYouDced(true);
if (data.opponentConnected === false) setOpponentConnected(false);
else if (data.opponentConnected === true && !data.opponentDced) setOpponentConnected(true);
if (data.status === 'completed') {
clearInterval(pollRef.current);
const you = data.players.you;
const won = data.youWon === true;
completedRef.current = true;
setDuration(you.timeSpentMs || (Date.now() - startTime));
setEloChange(you.eloChange || 0);
setPlayerWon(won);
if (won) {
setPhase('complete');
} else {
setShowDefeatOverlay(true);
}
} else {
const opp = data.players.opponent;
setOpponentProgress(opp.currentQuestion + 1);
}
})
.catch(() => {});
}, 1000);
}
function handleNext() {
const newAnswers = { ...answers, [qIdx]: selected };
setAnswers(newAnswers);
const correctCount = questions.filter((q, i) => newAnswers[i] === q.correct).length;
const elapsed = Date.now() - startTime;
if (qIdx < STAGE_SIZE - 1) {
submitProgress(qIdx + 1, correctCount, false);
const next = qIdx + 1;
setQIdx(next);
setSelected(newAnswers[next] ?? null);
} else {
submittedRef.current = true;
submitProgress(STAGE_SIZE, correctCount, true, elapsed);
setPhase('waiting');
}
}
function handleBack() {
const newAnswers = selected !== null ? { ...answers, [qIdx]: selected } : answers;
setAnswers(newAnswers);
const prev = qIdx - 1;
setQIdx(prev);
setSelected(newAnswers[prev] ?? null);
}
function handleJump(targetIdx) {
const newAnswers = selected !== null ? { ...answers, [qIdx]: selected } : answers;
setAnswers(newAnswers);
setQIdx(targetIdx);
setSelected(newAnswers[targetIdx] ?? null);
}
async function handleForfeit() {
if (!window.confirm('Are you sure you want to forfeit? This counts as a loss.')) return;
clearInterval(pollRef.current);
completedRef.current = true;
try {
await fetch(`${API}/game/forfeit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({ gameId }),
});
} catch {}
navigate('/dashboard');
}
if (phase === 'intro')
return <StageIntro grade={grade} myName={initMyName} myAvatar={initMyAvatar} opponentName={opponentName} opponentAvatar={opponentAvatar} onNext={() => { setPhase('question'); pollGameStatus(); }} />;
if (phase === 'question')
return (
<>
<QuestionScreen
question={questions[qIdx]} qIdx={qIdx} answers={answers} selected={selected}
onSelect={setSelected} onNext={handleNext} onBack={handleBack} onJump={handleJump}
opponentProgress={opponentProgress} opponentName={opponentName}
opponentDced={opponentDced} opponentConnected={opponentConnected}
onForfeit={handleForfeit}
/>
{showDefeatOverlay && (
<DefeatOverlay opponentName={opponentName} onViewResults={() => { setShowDefeatOverlay(false); setPhase('complete'); }} />
)}
</>
);
if (phase === 'waiting')
return (
<>
<WaitingScreen opponentName={opponentName} opponentProgress={opponentProgress} opponentDced={opponentDced} opponentConnected={opponentConnected} onForfeit={handleForfeit} />
{showDefeatOverlay && (
<DefeatOverlay opponentName={opponentName} onViewResults={() => { setShowDefeatOverlay(false); setPhase('complete'); }} />
)}
</>
);
if (phase === 'complete')
return (
<CompleteScreen
won={playerWon} opponentName={opponentName} duration={duration}
eloChange={eloChange} opponentDced={opponentDced} youDced={youDced} onDashboard={() => navigate('/dashboard')}
/>
);
return null;
}
+90
View File
@@ -1,14 +1,72 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTheme } from '../context/ThemeContext'; import { useTheme } from '../context/ThemeContext';
import UsernameModal from '../components/UsernameModal';
const API = import.meta.env.VITE_API_URL;
export default function Settings() { export default function Settings() {
const navigate = useNavigate(); const navigate = useNavigate();
const { theme, toggle } = useTheme(); const { theme, toggle } = useTheme();
const [token, setToken] = useState(null);
const [username, setUsername] = useState(null);
const [avatarUrl, setAvatarUrl] = useState(null);
const [usernameModal, setUsernameModal] = useState(false);
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef(null);
useEffect(() => {
const tok = sessionStorage.getItem('ranked_token');
if (!tok) { navigate('/'); return; }
setToken(tok);
}, [navigate]);
useEffect(() => {
if (!token) return;
fetch(`${API}/auth/me`, { headers: { Authorization: `Bearer ${token}` } })
.then(res => { if (res.status === 401) { sessionStorage.removeItem('ranked_token'); navigate('/'); return null; } return res.json(); })
.then(data => { if (data) { setUsername(data.username ?? null); setAvatarUrl(data.avatarUrl ?? null); } })
.catch(() => {});
}, [token, navigate]);
async function handleFileSelect(e) {
const file = e.target.files?.[0];
if (!file) return;
if (file.size > 2 * 1024 * 1024) { alert('Image too large. Max 2MB.'); return; }
setUploading(true);
const reader = new FileReader();
reader.onload = async (event) => {
try {
const res = await fetch(`${API}/me/avatar`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ image: event.target.result }),
});
const data = await res.json();
if (res.ok) setAvatarUrl(data.avatarUrl);
else alert(data.error || 'Upload failed');
} catch {
alert('Could not upload. Check your connection.');
} finally {
setUploading(false);
}
};
reader.readAsDataURL(file);
}
return ( return (
<> <>
<div className="arena-bg"><div className="arena-grain" /></div> <div className="arena-bg"><div className="arena-grain" /></div>
{usernameModal && (
<UsernameModal
token={token}
current={username}
onClose={() => setUsernameModal(false)}
onSave={(u) => setUsername(u)}
/>
)}
<nav className="dash-nav"> <nav className="dash-nav">
<span className="dash-nav__logo">RANKED<span>EQAO</span></span> <span className="dash-nav__logo">RANKED<span>EQAO</span></span>
<button className="settings-back" onClick={() => navigate('/dashboard')}>Back</button> <button className="settings-back" onClick={() => navigate('/dashboard')}>Back</button>
@@ -19,6 +77,38 @@ export default function Settings() {
<h1 className="settings-title">Settings</h1> <h1 className="settings-title">Settings</h1>
<div className="settings-card"> <div className="settings-card">
<div className="settings-row">
<div className="settings-row__info">
<span className="settings-row__label">Profile Picture</span>
<span className="settings-row__desc">
{avatarUrl ? 'Click to change' : 'Upload a photo'}
</span>
</div>
<div className="settings-avatar" onClick={() => fileInputRef.current?.click()}>
{avatarUrl ? (
<img className="settings-avatar__img" src={avatarUrl} alt="Avatar" referrerPolicy="no-referrer" />
) : (
<div className="settings-avatar__placeholder">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</div>
)}
{uploading && <div className="settings-avatar__spinner" />}
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleFileSelect} style={{ display: 'none' }} />
</div>
</div>
<div className="settings-row">
<div className="settings-row__info">
<span className="settings-row__label">Username</span>
<span className="settings-row__desc">
{username ? `@${username}` : 'Not set \u2014 choose a unique name'}
</span>
</div>
<button className="settings-row__action" onClick={() => setUsernameModal(true)}>
{username ? 'Change' : 'Set'}
</button>
</div>
<div className="settings-row"> <div className="settings-row">
<div className="settings-row__info"> <div className="settings-row__info">
<span className="settings-row__label">Theme</span> <span className="settings-row__label">Theme</span>
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../mime/cli.js
Generated Vendored
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mime\cli.js" %*
Generated Vendored
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../mime/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../mime/cli.js" $args
} else {
& "node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../prisma/build/index.js
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\prisma\build\index.js" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../prisma/build/index.js" $args
} else {
& "$basedir/node$exe" "$basedir/../prisma/build/index.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../prisma/build/index.js" $args
} else {
& "node$exe" "$basedir/../prisma/build/index.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../semver/bin/semver.js
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\semver\bin\semver.js" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1 @@
b72732897f7fb1d24ea9d26cb254b78b10d4257eaeb27f04d9fb82d20addc5d5
@@ -0,0 +1 @@
7d58cada77c5833e57d2ab4ad61ea2948247b2caa8575066b2fe3bc7e4ea4e5a
@@ -0,0 +1 @@
cfdcce35f151ea8e57772f07fd909b6118389119b76e51ab1105ef86f955048b
@@ -0,0 +1 @@
a7d949e16cc5937aa77d67888c8993118ef16c764e536e9ed7c17cfe61bb65ad
+1151
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
export * from "./index"
+1
View File
@@ -0,0 +1 @@
module.exports = { ...require('.') }
+9
View File
@@ -0,0 +1,9 @@
class PrismaClient {
constructor() {
throw new Error(
'@prisma/client/deno/edge did not initialize yet. Please run "prisma generate" and try to import it again.',
)
}
}
export { PrismaClient }
+1
View File
@@ -0,0 +1 @@
export * from "./default"
+288
View File
File diff suppressed because one or more lines are too long
+280
View File
@@ -0,0 +1,280 @@
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('@prisma/client/runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 5.22.0
* Query Engine version: 605197351a3c8bdd595af2d2a9bc3025bca48ea2
*/
Prisma.prismaVersion = {
client: "5.22.0",
engine: "605197351a3c8bdd595af2d2a9bc3025bca48ea2"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.NotFoundError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`NotFoundError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
googleId: 'googleId',
email: 'email',
displayName: 'displayName',
username: 'username',
avatarUrl: 'avatarUrl',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.QuestionScalarFieldEnum = {
id: 'id',
grade: 'grade',
subject: 'subject',
strand: 'strand',
difficulty: 'difficulty',
questionText: 'questionText',
options: 'options',
correctAnswer: 'correctAnswer',
explanation: 'explanation',
createdAt: 'createdAt'
};
exports.Prisma.GameSessionScalarFieldEnum = {
id: 'id',
userId: 'userId',
grade: 'grade',
subject: 'subject',
status: 'status',
score: 'score',
totalQuestions: 'totalQuestions',
correctAnswers: 'correctAnswers',
startedAt: 'startedAt',
completedAt: 'completedAt'
};
exports.Prisma.UserAnswerScalarFieldEnum = {
id: 'id',
sessionId: 'sessionId',
questionId: 'questionId',
selectedAnswer: 'selectedAnswer',
isCorrect: 'isCorrect',
timeSpentMs: 'timeSpentMs',
answeredAt: 'answeredAt'
};
exports.Prisma.LeaderboardEntryScalarFieldEnum = {
id: 'id',
userId: 'userId',
grade: 'grade',
elo: 'elo',
bestTime: 'bestTime',
gamesPlayed: 'gamesPlayed',
totalCorrect: 'totalCorrect',
updatedAt: 'updatedAt'
};
exports.Prisma.MatchResultScalarFieldEnum = {
id: 'id',
userId: 'userId',
grade: 'grade',
won: 'won',
eloChange: 'eloChange',
eloAfter: 'eloAfter',
timeSpentMs: 'timeSpentMs',
playedAt: 'playedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.JsonNullValueInput = {
JsonNull: Prisma.JsonNull
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.JsonNullValueFilter = {
DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull,
AnyNull: Prisma.AnyNull
};
exports.Grade = exports.$Enums.Grade = {
G3: 'G3',
G6: 'G6',
G9: 'G9'
};
exports.Subject = exports.$Enums.Subject = {
MATH: 'MATH',
READING: 'READING',
WRITING: 'WRITING'
};
exports.Difficulty = exports.$Enums.Difficulty = {
EASY: 'EASY',
MEDIUM: 'MEDIUM',
HARD: 'HARD'
};
exports.SessionStatus = exports.$Enums.SessionStatus = {
IN_PROGRESS: 'IN_PROGRESS',
COMPLETED: 'COMPLETED',
ABANDONED: 'ABANDONED'
};
exports.Prisma.ModelName = {
User: 'User',
Question: 'Question',
GameSession: 'GameSession',
UserAnswer: 'UserAnswer',
LeaderboardEntry: 'LeaderboardEntry',
MatchResult: 'MatchResult'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
+10914
View File
File diff suppressed because it is too large Load Diff
+309
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
+97
View File
@@ -0,0 +1,97 @@
{
"name": "prisma-client-c825c9bffeb45b3764e8e215c3deab0c9bee94086203bf71c8424a6b8e826ad3",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
"exports": {
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.js",
"default": "./wasm.js"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.js",
"default": "./runtime/library.js"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.js",
"default": "./runtime/binary.js"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "5.22.0",
"sideEffects": false
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+119
View File
@@ -0,0 +1,119 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Grade {
G3
G6
G9
}
enum Subject {
MATH
READING
WRITING
}
enum Difficulty {
EASY
MEDIUM
HARD
}
enum SessionStatus {
IN_PROGRESS
COMPLETED
ABANDONED
}
model User {
id String @id @default(cuid())
googleId String @unique
email String @unique
displayName String
username String @unique
avatarUrl String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions GameSession[]
leaderboardEntries LeaderboardEntry[]
matchResults MatchResult[]
}
model Question {
id String @id @default(cuid())
grade Grade
subject Subject
strand String
difficulty Difficulty
questionText String
options Json
correctAnswer String
explanation String?
createdAt DateTime @default(now())
answers UserAnswer[]
}
model GameSession {
id String @id @default(cuid())
userId String
grade Grade
subject Subject
status SessionStatus @default(IN_PROGRESS)
score Int @default(0)
totalQuestions Int
correctAnswers Int @default(0)
startedAt DateTime @default(now())
completedAt DateTime?
user User @relation(fields: [userId], references: [id])
answers UserAnswer[]
}
model UserAnswer {
id String @id @default(cuid())
sessionId String
questionId String
selectedAnswer String
isCorrect Boolean
timeSpentMs Int
answeredAt DateTime @default(now())
session GameSession @relation(fields: [sessionId], references: [id])
question Question @relation(fields: [questionId], references: [id])
}
model LeaderboardEntry {
id String @id @default(cuid())
userId String
grade Grade
elo Int @default(800)
bestTime Int?
gamesPlayed Int @default(0)
totalCorrect Int @default(0)
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@unique([userId, grade])
}
model MatchResult {
id String @id @default(cuid())
userId String
grade Grade
won Boolean
eloChange Int
eloAfter Int
timeSpentMs Int?
playedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
+1
View File
@@ -0,0 +1 @@
export * from "./index"
+280
View File
@@ -0,0 +1,280 @@
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('@prisma/client/runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 5.22.0
* Query Engine version: 605197351a3c8bdd595af2d2a9bc3025bca48ea2
*/
Prisma.prismaVersion = {
client: "5.22.0",
engine: "605197351a3c8bdd595af2d2a9bc3025bca48ea2"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.NotFoundError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`NotFoundError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
googleId: 'googleId',
email: 'email',
displayName: 'displayName',
username: 'username',
avatarUrl: 'avatarUrl',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.QuestionScalarFieldEnum = {
id: 'id',
grade: 'grade',
subject: 'subject',
strand: 'strand',
difficulty: 'difficulty',
questionText: 'questionText',
options: 'options',
correctAnswer: 'correctAnswer',
explanation: 'explanation',
createdAt: 'createdAt'
};
exports.Prisma.GameSessionScalarFieldEnum = {
id: 'id',
userId: 'userId',
grade: 'grade',
subject: 'subject',
status: 'status',
score: 'score',
totalQuestions: 'totalQuestions',
correctAnswers: 'correctAnswers',
startedAt: 'startedAt',
completedAt: 'completedAt'
};
exports.Prisma.UserAnswerScalarFieldEnum = {
id: 'id',
sessionId: 'sessionId',
questionId: 'questionId',
selectedAnswer: 'selectedAnswer',
isCorrect: 'isCorrect',
timeSpentMs: 'timeSpentMs',
answeredAt: 'answeredAt'
};
exports.Prisma.LeaderboardEntryScalarFieldEnum = {
id: 'id',
userId: 'userId',
grade: 'grade',
elo: 'elo',
bestTime: 'bestTime',
gamesPlayed: 'gamesPlayed',
totalCorrect: 'totalCorrect',
updatedAt: 'updatedAt'
};
exports.Prisma.MatchResultScalarFieldEnum = {
id: 'id',
userId: 'userId',
grade: 'grade',
won: 'won',
eloChange: 'eloChange',
eloAfter: 'eloAfter',
timeSpentMs: 'timeSpentMs',
playedAt: 'playedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.JsonNullValueInput = {
JsonNull: Prisma.JsonNull
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.JsonNullValueFilter = {
DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull,
AnyNull: Prisma.AnyNull
};
exports.Grade = exports.$Enums.Grade = {
G3: 'G3',
G6: 'G6',
G9: 'G9'
};
exports.Subject = exports.$Enums.Subject = {
MATH: 'MATH',
READING: 'READING',
WRITING: 'WRITING'
};
exports.Difficulty = exports.$Enums.Difficulty = {
EASY: 'EASY',
MEDIUM: 'MEDIUM',
HARD: 'HARD'
};
exports.SessionStatus = exports.$Enums.SessionStatus = {
IN_PROGRESS: 'IN_PROGRESS',
COMPLETED: 'COMPLETED',
ABANDONED: 'ABANDONED'
};
exports.Prisma.ModelName = {
User: 'User',
Question: 'Question',
GameSession: 'GameSession',
UserAnswer: 'UserAnswer',
LeaderboardEntry: 'LeaderboardEntry',
MatchResult: 'MatchResult'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+27
View File
@@ -0,0 +1,27 @@
# Prisma Client &middot; [![npm version](https://img.shields.io/npm/v/@prisma/client.svg?style=flat)](https://www.npmjs.com/package/@prisma/client) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/prisma/prisma/blob/main/CONTRIBUTING.md) [![GitHub license](https://img.shields.io/badge/license-Apache%202-blue)](https://github.com/prisma/prisma/blob/main/LICENSE) [![Discord](https://img.shields.io/discord/937751382725886062?label=Discord)](https://pris.ly/discord)
Prisma Client JS is an **auto-generated query builder** that enables **type-safe** database access and **reduces boilerplate**. You can use it as an alternative to traditional ORMs such as Sequelize, TypeORM or SQL query builders like knex.js.
It is part of the [Prisma](https://www.prisma.io/) ecosystem. Prisma provides database tools for data access, declarative data modeling, schema migrations and visual data management. Learn more in the main [`prisma`](https://github.com/prisma/prisma/) repository or read the [documentation](https://www.prisma.io/docs/).
## Getting started
Follow one of these guides to get started with Prisma Client JS:
- [Quickstart](https://www.prisma.io/docs/getting-started/quickstart) (5 min)
- [Set up a new project with Prisma (SQL migrations)](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-sql) (15 min)
- [Set up a new project with Prisma (Prisma Migrate)](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-prisma-migrate) (15 min)
- [Add Prisma to an existing project](https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project) (15 min)
Alternatively you can explore the ready-to-run [examples](https://github.com/prisma/prisma-examples/) (REST, GraphQL, gRPC, plain JavaScript and TypeScript demos, ...) or watch the [demo videos](https://www.youtube.com/watch?v=0RhtQgIs-TE&list=PLn2e1F9Rfr6k9PnR_figWOcSHgc_erDr5&index=1) (1-2 min per video).
## Contributing
Refer to our [contribution guidelines](https://github.com/prisma/prisma/blob/main/CONTRIBUTING.md) and [Code of Conduct for contributors](https://github.com/prisma/prisma/blob/main/CODE_OF_CONDUCT.md).
## Tests Status
- Prisma Tests Status:
[![CI](https://github.com/prisma/prisma/actions/workflows/test.yml/badge.svg)](https://github.com/prisma/prisma/actions/workflows/test.yml)
- Ecosystem Tests Status:
[![Actions Status](https://github.com/prisma/ecosystem-tests/workflows/test/badge.svg)](https://github.com/prisma/ecosystem-tests/actions)
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/default'
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
...require('.prisma/client/default'),
}
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/edge'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('.prisma/client/edge'),
}
+1
View File
@@ -0,0 +1 @@
export * from './scripts/default-index'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('./scripts/default-index'),
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
const prisma = require('.prisma/client/index-browser')
module.exports = prisma
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/default'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('.prisma/client/default'),
}
+280
View File
@@ -0,0 +1,280 @@
{
"name": "@prisma/client",
"version": "5.22.0",
"description": "Prisma Client is an auto-generated, type-safe and modern JavaScript/TypeScript ORM for Node.js that's tailored to your data. Supports PostgreSQL, CockroachDB, MySQL, MariaDB, SQL Server, SQLite & MongoDB databases.",
"keywords": [
"ORM",
"Prisma",
"prisma2",
"Prisma Client",
"client",
"query",
"query-builder",
"database",
"db",
"JavaScript",
"JS",
"TypeScript",
"TS",
"SQL",
"SQLite",
"pg",
"Postgres",
"PostgreSQL",
"CockroachDB",
"MySQL",
"MariaDB",
"MSSQL",
"SQL Server",
"SQLServer",
"MongoDB",
"react-native"
],
"main": "default.js",
"types": "default.d.ts",
"browser": "index-browser.js",
"exports": {
"./package.json": "./package.json",
".": {
"require": {
"types": "./default.d.ts",
"node": "./default.js",
"edge-light": "./default.js",
"workerd": "./default.js",
"worker": "./default.js",
"browser": "./index-browser.js"
},
"import": {
"types": "./default.d.ts",
"node": "./default.js",
"edge-light": "./default.js",
"workerd": "./default.js",
"worker": "./default.js",
"browser": "./index-browser.js"
},
"default": "./default.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.js",
"default": "./wasm.js"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.js",
"default": "./runtime/library.js"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.js",
"default": "./runtime/binary.js"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"license": "Apache-2.0",
"engines": {
"node": ">=16.13"
},
"homepage": "https://www.prisma.io",
"repository": {
"type": "git",
"url": "https://github.com/prisma/prisma.git",
"directory": "packages/client"
},
"author": "Tim Suchanek <suchanek@prisma.io>",
"bugs": "https://github.com/prisma/prisma/issues",
"files": [
"README.md",
"runtime",
"!runtime/*.map",
"scripts",
"generator-build",
"edge.js",
"edge.d.ts",
"wasm.js",
"wasm.d.ts",
"index.js",
"index.d.ts",
"react-native.js",
"react-native.d.ts",
"default.js",
"default.d.ts",
"index-browser.js",
"extension.js",
"extension.d.ts",
"sql.d.ts",
"sql.js",
"sql.mjs"
],
"devDependencies": {
"@cloudflare/workers-types": "4.20240614.0",
"@codspeed/benchmark.js-plugin": "3.1.1",
"@faker-js/faker": "8.4.1",
"@fast-check/jest": "1.8.2",
"@inquirer/prompts": "5.0.5",
"@jest/create-cache-key-function": "29.7.0",
"@jest/globals": "29.7.0",
"@jest/test-sequencer": "29.7.0",
"@libsql/client": "0.8.0",
"@neondatabase/serverless": "0.9.3",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "1.25.1",
"@opentelemetry/instrumentation": "0.52.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1",
"@planetscale/database": "1.18.0",
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"@prisma/mini-proxy": "0.9.5",
"@prisma/query-engine-wasm": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"@snaplet/copycat": "0.17.3",
"@swc-node/register": "1.10.9",
"@swc/core": "1.6.13",
"@swc/jest": "0.2.36",
"@timsuchanek/copy": "1.4.5",
"@types/debug": "4.1.12",
"@types/fs-extra": "9.0.13",
"@types/jest": "29.5.12",
"@types/js-levenshtein": "1.1.3",
"@types/mssql": "9.1.5",
"@types/node": "18.19.31",
"@types/pg": "8.11.6",
"arg": "5.0.2",
"benchmark": "2.1.4",
"ci-info": "4.0.0",
"decimal.js": "10.4.3",
"detect-runtime": "1.0.4",
"env-paths": "2.2.1",
"esbuild": "0.23.0",
"execa": "5.1.1",
"expect-type": "0.19.0",
"flat-map-polyfill": "0.3.8",
"fs-extra": "11.1.1",
"get-stream": "6.0.1",
"globby": "11.1.0",
"indent-string": "4.0.0",
"jest": "29.7.0",
"jest-extended": "4.0.2",
"jest-junit": "16.0.0",
"jest-serializer-ansi-escapes": "3.0.0",
"jest-snapshot": "29.7.0",
"js-levenshtein": "1.1.6",
"kleur": "4.1.5",
"klona": "2.0.6",
"mariadb": "3.3.1",
"memfs": "4.9.3",
"mssql": "11.0.1",
"new-github-issue-url": "0.2.1",
"node-fetch": "3.3.2",
"p-retry": "4.6.2",
"pg": "8.11.5",
"pkg-up": "3.1.0",
"pluralize": "8.0.0",
"resolve": "1.22.8",
"rimraf": "3.0.2",
"simple-statistics": "7.8.5",
"sort-keys": "4.2.0",
"source-map-support": "0.5.21",
"sql-template-tag": "5.2.1",
"stacktrace-parser": "0.1.10",
"strip-ansi": "6.0.1",
"strip-indent": "3.0.0",
"ts-node": "10.9.2",
"ts-pattern": "5.2.0",
"tsd": "0.31.1",
"typescript": "5.4.5",
"undici": "5.28.4",
"wrangler": "3.62.0",
"zx": "7.2.3",
"@prisma/adapter-d1": "5.22.0",
"@prisma/adapter-libsql": "5.22.0",
"@prisma/adapter-neon": "5.22.0",
"@prisma/adapter-pg": "5.22.0",
"@prisma/adapter-planetscale": "5.22.0",
"@prisma/driver-adapter-utils": "5.22.0",
"@prisma/adapter-pg-worker": "5.22.0",
"@prisma/debug": "5.22.0",
"@prisma/engines": "5.22.0",
"@prisma/fetch-engine": "5.22.0",
"@prisma/generator-helper": "5.22.0",
"@prisma/get-platform": "5.22.0",
"@prisma/instrumentation": "5.22.0",
"@prisma/internals": "5.22.0",
"@prisma/migrate": "5.22.0",
"@prisma/pg-worker": "5.22.0"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
},
"sideEffects": false,
"scripts": {
"dev": "DEV=true tsx helpers/build.ts",
"build": "tsx helpers/build.ts",
"test": "dotenv -e ../../.db.env -- jest --silent",
"test:e2e": "dotenv -e ../../.db.env -- tsx tests/e2e/_utils/run.ts",
"test:functional": "dotenv -e ../../.db.env -- tsx helpers/functional-test/run-tests.ts",
"test:memory": "dotenv -e ../../.db.env -- tsx helpers/memory-tests.ts",
"test:functional:code": "dotenv -e ../../.db.env -- tsx helpers/functional-test/run-tests.ts --no-types",
"test:functional:types": "dotenv -e ../../.db.env -- tsx helpers/functional-test/run-tests.ts --types-only",
"test-notypes": "dotenv -e ../../.db.env -- jest --testPathIgnorePatterns src/__tests__/types/types.test.ts",
"generate": "node scripts/postinstall.js",
"postinstall": "node scripts/postinstall.js",
"new-test": "tsx ./helpers/new-test/new-test.ts"
}
}
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/react-native'
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
...require('.prisma/client/react-native'),
}
+1
View File
@@ -0,0 +1 @@
export * from "./library"
+210
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+31
View File
File diff suppressed because one or more lines are too long
+365
View File
@@ -0,0 +1,365 @@
declare class AnyNull extends NullTypesEnumValue {
}
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
types: {
operations: {
[K in F]: {
args: any;
};
};
};
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
declare class DbNull extends NullTypesEnumValue {
}
export declare namespace Decimal {
export type Constructor = typeof Decimal;
export type Instance = Decimal;
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type Modulo = Rounding | 9;
export type Value = string | number | Decimal;
// http://mikemcl.github.io/decimal.js/#constructor-properties
export interface Config {
precision?: number;
rounding?: Rounding;
toExpNeg?: number;
toExpPos?: number;
minE?: number;
maxE?: number;
crypto?: boolean;
modulo?: Modulo;
defaults?: boolean;
}
}
export declare class Decimal {
readonly d: number[];
readonly e: number;
readonly s: number;
constructor(n: Decimal.Value);
absoluteValue(): Decimal;
abs(): Decimal;
ceil(): Decimal;
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
comparedTo(n: Decimal.Value): number;
cmp(n: Decimal.Value): number;
cosine(): Decimal;
cos(): Decimal;
cubeRoot(): Decimal;
cbrt(): Decimal;
decimalPlaces(): number;
dp(): number;
dividedBy(n: Decimal.Value): Decimal;
div(n: Decimal.Value): Decimal;
dividedToIntegerBy(n: Decimal.Value): Decimal;
divToInt(n: Decimal.Value): Decimal;
equals(n: Decimal.Value): boolean;
eq(n: Decimal.Value): boolean;
floor(): Decimal;
greaterThan(n: Decimal.Value): boolean;
gt(n: Decimal.Value): boolean;
greaterThanOrEqualTo(n: Decimal.Value): boolean;
gte(n: Decimal.Value): boolean;
hyperbolicCosine(): Decimal;
cosh(): Decimal;
hyperbolicSine(): Decimal;
sinh(): Decimal;
hyperbolicTangent(): Decimal;
tanh(): Decimal;
inverseCosine(): Decimal;
acos(): Decimal;
inverseHyperbolicCosine(): Decimal;
acosh(): Decimal;
inverseHyperbolicSine(): Decimal;
asinh(): Decimal;
inverseHyperbolicTangent(): Decimal;
atanh(): Decimal;
inverseSine(): Decimal;
asin(): Decimal;
inverseTangent(): Decimal;
atan(): Decimal;
isFinite(): boolean;
isInteger(): boolean;
isInt(): boolean;
isNaN(): boolean;
isNegative(): boolean;
isNeg(): boolean;
isPositive(): boolean;
isPos(): boolean;
isZero(): boolean;
lessThan(n: Decimal.Value): boolean;
lt(n: Decimal.Value): boolean;
lessThanOrEqualTo(n: Decimal.Value): boolean;
lte(n: Decimal.Value): boolean;
logarithm(n?: Decimal.Value): Decimal;
log(n?: Decimal.Value): Decimal;
minus(n: Decimal.Value): Decimal;
sub(n: Decimal.Value): Decimal;
modulo(n: Decimal.Value): Decimal;
mod(n: Decimal.Value): Decimal;
naturalExponential(): Decimal;
exp(): Decimal;
naturalLogarithm(): Decimal;
ln(): Decimal;
negated(): Decimal;
neg(): Decimal;
plus(n: Decimal.Value): Decimal;
add(n: Decimal.Value): Decimal;
precision(includeZeros?: boolean): number;
sd(includeZeros?: boolean): number;
round(): Decimal;
sine() : Decimal;
sin() : Decimal;
squareRoot(): Decimal;
sqrt(): Decimal;
tangent() : Decimal;
tan() : Decimal;
times(n: Decimal.Value): Decimal;
mul(n: Decimal.Value) : Decimal;
toBinary(significantDigits?: number): string;
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
toDecimalPlaces(decimalPlaces?: number): Decimal;
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toDP(decimalPlaces?: number): Decimal;
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toExponential(decimalPlaces?: number): string;
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFixed(decimalPlaces?: number): string;
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFraction(max_denominator?: Decimal.Value): Decimal[];
toHexadecimal(significantDigits?: number): string;
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
toHex(significantDigits?: number): string;
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
toJSON(): string;
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
toNumber(): number;
toOctal(significantDigits?: number): string;
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
toPower(n: Decimal.Value): Decimal;
pow(n: Decimal.Value): Decimal;
toPrecision(significantDigits?: number): string;
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
toSignificantDigits(significantDigits?: number): Decimal;
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toSD(significantDigits?: number): Decimal;
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toString(): string;
truncated(): Decimal;
trunc(): Decimal;
valueOf(): string;
static abs(n: Decimal.Value): Decimal;
static acos(n: Decimal.Value): Decimal;
static acosh(n: Decimal.Value): Decimal;
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
static asin(n: Decimal.Value): Decimal;
static asinh(n: Decimal.Value): Decimal;
static atan(n: Decimal.Value): Decimal;
static atanh(n: Decimal.Value): Decimal;
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
static cbrt(n: Decimal.Value): Decimal;
static ceil(n: Decimal.Value): Decimal;
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
static clone(object?: Decimal.Config): Decimal.Constructor;
static config(object: Decimal.Config): Decimal.Constructor;
static cos(n: Decimal.Value): Decimal;
static cosh(n: Decimal.Value): Decimal;
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
static exp(n: Decimal.Value): Decimal;
static floor(n: Decimal.Value): Decimal;
static hypot(...n: Decimal.Value[]): Decimal;
static isDecimal(object: any): object is Decimal;
static ln(n: Decimal.Value): Decimal;
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
static log2(n: Decimal.Value): Decimal;
static log10(n: Decimal.Value): Decimal;
static max(...n: Decimal.Value[]): Decimal;
static min(...n: Decimal.Value[]): Decimal;
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
static noConflict(): Decimal.Constructor; // Browser only
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
static random(significantDigits?: number): Decimal;
static round(n: Decimal.Value): Decimal;
static set(object: Decimal.Config): Decimal.Constructor;
static sign(n: Decimal.Value): number;
static sin(n: Decimal.Value): Decimal;
static sinh(n: Decimal.Value): Decimal;
static sqrt(n: Decimal.Value): Decimal;
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
static sum(...n: Decimal.Value[]): Decimal;
static tan(n: Decimal.Value): Decimal;
static tanh(n: Decimal.Value): Decimal;
static trunc(n: Decimal.Value): Decimal;
static readonly default?: Decimal.Constructor;
static readonly Decimal?: Decimal.Constructor;
static readonly precision: number;
static readonly rounding: Decimal.Rounding;
static readonly toExpNeg: number;
static readonly toExpPos: number;
static readonly minE: number;
static readonly maxE: number;
static readonly crypto: boolean;
static readonly modulo: Decimal.Modulo;
static readonly ROUND_UP: 0;
static readonly ROUND_DOWN: 1;
static readonly ROUND_CEIL: 2;
static readonly ROUND_FLOOR: 3;
static readonly ROUND_HALF_UP: 4;
static readonly ROUND_HALF_DOWN: 5;
static readonly ROUND_HALF_EVEN: 6;
static readonly ROUND_HALF_CEIL: 7;
static readonly ROUND_HALF_FLOOR: 8;
static readonly EUCLID: 9;
}
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
} : W) : never) | (A extends Narrowable ? A : never);
export declare function getRuntime(): GetRuntimeOutput;
declare type GetRuntimeOutput = {
id: Runtime;
prettyName: string;
isEdge: boolean;
};
declare class JsonNull extends NullTypesEnumValue {
}
/**
* Generates more strict variant of an enum which, unlike regular enum,
* throws on non-existing property access. This can be useful in following situations:
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
* - enum values are generated dynamically from DMMF.
*
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
* will result in `undefined` value being used, which will be accepted. Using strict enum
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
*
* Note: if you need to check for existence of a value in the enum you can still use either
* `in` operator or `hasOwnProperty` function.
*
* @param definition
* @returns
*/
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
declare type Narrowable = string | number | bigint | boolean | [];
declare class NullTypesEnumValue extends ObjectEnumValue {
_getNamespace(): string;
}
/**
* Base class for unique values of object-valued enums.
*/
declare abstract class ObjectEnumValue {
constructor(arg?: symbol);
abstract _getNamespace(): string;
_getName(): string;
toString(): string;
}
export declare const objectEnumValues: {
classes: {
DbNull: typeof DbNull;
JsonNull: typeof JsonNull;
AnyNull: typeof AnyNull;
};
instances: {
DbNull: DbNull;
JsonNull: JsonNull;
AnyNull: AnyNull;
};
};
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
declare namespace Public {
export {
validator
}
}
export { Public }
declare type Runtime = "edge-routine" | "workerd" | "deno" | "lagon" | "react-native" | "netlify" | "electron" | "node" | "bun" | "edge-light" | "fastly" | "unknown";
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
export { }
File diff suppressed because one or more lines are too long
+3403
View File
File diff suppressed because it is too large Load Diff
+143
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+32
View File
File diff suppressed because one or more lines are too long
+176
View File
@@ -0,0 +1,176 @@
'use strict'
const isObject = (val) => val !== null && typeof val === 'object' && !Array.isArray(val)
// this is a modified version of https://github.com/chalk/ansi-regex (MIT License)
const ANSI_REGEX =
/* eslint-disable-next-line no-control-regex */
/[\u001b\u009b][[\]#;?()]*(?:(?:(?:[^\W_]*;?[^\W_]*)\u0007)|(?:(?:[0-9]{1,4}(;[0-9]{0,4})*)?[~0-9=<>cf-nqrtyA-PRZ]))/g
const create = () => {
const colors = { enabled: true, visible: true, styles: {}, keys: {} }
if ('FORCE_COLOR' in process.env) {
colors.enabled = process.env.FORCE_COLOR !== '0'
}
const ansi = (style) => {
let open = (style.open = `\u001b[${style.codes[0]}m`)
let close = (style.close = `\u001b[${style.codes[1]}m`)
let regex = (style.regex = new RegExp(`\\u001b\\[${style.codes[1]}m`, 'g'))
style.wrap = (input, newline) => {
if (input.includes(close)) input = input.replace(regex, close + open)
let output = open + input + close
// see https://github.com/chalk/chalk/pull/92, thanks to the
// chalk contributors for this fix. However, we've confirmed that
// this issue is also present in Windows terminals
return newline ? output.replace(/\r*\n/g, `${close}$&${open}`) : output
}
return style
}
const wrap = (style, input, newline) => {
return typeof style === 'function' ? style(input) : style.wrap(input, newline)
}
const style = (input, stack) => {
if (input === '' || input == null) return ''
if (colors.enabled === false) return input
if (colors.visible === false) return ''
let str = '' + input
let nl = str.includes('\n')
let n = stack.length
if (n > 0 && stack.includes('unstyle')) {
stack = [...new Set(['unstyle', ...stack])].reverse()
}
while (n-- > 0) str = wrap(colors.styles[stack[n]], str, nl)
return str
}
const define = (name, codes, type) => {
colors.styles[name] = ansi({ name, codes })
let keys = colors.keys[type] || (colors.keys[type] = [])
keys.push(name)
Reflect.defineProperty(colors, name, {
configurable: true,
enumerable: true,
set(value) {
colors.alias(name, value)
},
get() {
let color = (input) => style(input, color.stack)
Reflect.setPrototypeOf(color, colors)
color.stack = this.stack ? this.stack.concat(name) : [name]
return color
},
})
}
define('reset', [0, 0], 'modifier')
define('bold', [1, 22], 'modifier')
define('dim', [2, 22], 'modifier')
define('italic', [3, 23], 'modifier')
define('underline', [4, 24], 'modifier')
define('inverse', [7, 27], 'modifier')
define('hidden', [8, 28], 'modifier')
define('strikethrough', [9, 29], 'modifier')
define('black', [30, 39], 'color')
define('red', [31, 39], 'color')
define('green', [32, 39], 'color')
define('yellow', [33, 39], 'color')
define('blue', [34, 39], 'color')
define('magenta', [35, 39], 'color')
define('cyan', [36, 39], 'color')
define('white', [37, 39], 'color')
define('gray', [90, 39], 'color')
define('grey', [90, 39], 'color')
define('bgBlack', [40, 49], 'bg')
define('bgRed', [41, 49], 'bg')
define('bgGreen', [42, 49], 'bg')
define('bgYellow', [43, 49], 'bg')
define('bgBlue', [44, 49], 'bg')
define('bgMagenta', [45, 49], 'bg')
define('bgCyan', [46, 49], 'bg')
define('bgWhite', [47, 49], 'bg')
define('blackBright', [90, 39], 'bright')
define('redBright', [91, 39], 'bright')
define('greenBright', [92, 39], 'bright')
define('yellowBright', [93, 39], 'bright')
define('blueBright', [94, 39], 'bright')
define('magentaBright', [95, 39], 'bright')
define('cyanBright', [96, 39], 'bright')
define('whiteBright', [97, 39], 'bright')
define('bgBlackBright', [100, 49], 'bgBright')
define('bgRedBright', [101, 49], 'bgBright')
define('bgGreenBright', [102, 49], 'bgBright')
define('bgYellowBright', [103, 49], 'bgBright')
define('bgBlueBright', [104, 49], 'bgBright')
define('bgMagentaBright', [105, 49], 'bgBright')
define('bgCyanBright', [106, 49], 'bgBright')
define('bgWhiteBright', [107, 49], 'bgBright')
colors.ansiRegex = ANSI_REGEX
colors.hasColor = colors.hasAnsi = (str) => {
colors.ansiRegex.lastIndex = 0
return typeof str === 'string' && str !== '' && colors.ansiRegex.test(str)
}
colors.alias = (name, color) => {
let fn = typeof color === 'string' ? colors[color] : color
if (typeof fn !== 'function') {
throw new TypeError('Expected alias to be the name of an existing color (string) or a function')
}
if (!fn.stack) {
Reflect.defineProperty(fn, 'name', { value: name })
colors.styles[name] = fn
fn.stack = [name]
}
Reflect.defineProperty(colors, name, {
configurable: true,
enumerable: true,
set(value) {
colors.alias(name, value)
},
get() {
let color = (input) => style(input, color.stack)
Reflect.setPrototypeOf(color, colors)
color.stack = this.stack ? this.stack.concat(fn.stack) : fn.stack
return color
},
})
}
colors.theme = (custom) => {
if (!isObject(custom)) throw new TypeError('Expected theme to be an object')
for (let name of Object.keys(custom)) {
colors.alias(name, custom[name])
}
return colors
}
colors.alias('unstyle', (str) => {
if (typeof str === 'string' && str !== '') {
colors.ansiRegex.lastIndex = 0
return str.replace(colors.ansiRegex, '')
}
return ''
})
colors.alias('noop', (str) => str)
colors.none = colors.clear = colors.noop
colors.stripColor = colors.unstyle
colors.define = define
return colors
}
module.exports = create()
module.exports.create = create
+9
View File
@@ -0,0 +1,9 @@
class PrismaClient {
constructor() {
throw new Error(
'@prisma/client/deno/edge did not initialize yet. Please run "prisma generate" and try to import it again.',
)
}
}
export { PrismaClient }
+110
View File
@@ -0,0 +1,110 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as runtime from '@prisma/client/runtime/library'
/**
* ## Prisma Client ʲˢ
*
* Type-safe database client for TypeScript & Node.js
* @example
* ```
* const prisma = new Prisma()
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
*
*
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client).
*/
export declare const PrismaClient: any
/**
* ## Prisma Client ʲˢ
*
* Type-safe database client for TypeScript & Node.js
* @example
* ```
* const prisma = new Prisma()
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
*
*
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client).
*/
export declare type PrismaClient = any
export declare class PrismaClientExtends<
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
> {
$extends: { extArgs: ExtArgs } & (<
R extends runtime.Types.Extensions.UserArgs['result'] = {},
M extends runtime.Types.Extensions.UserArgs['model'] = {},
Q extends runtime.Types.Extensions.UserArgs['query'] = {},
C extends runtime.Types.Extensions.UserArgs['client'] = {},
Args extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.InternalArgs<R, M, {}, C>,
>(
args:
| ((client: PrismaClientExtends<ExtArgs>) => { $extends: { extArgs: Args } })
| { name?: string }
| { result?: R & runtime.Types.Extensions.UserArgs['result'] }
| { model?: M & runtime.Types.Extensions.UserArgs['model'] }
| { query?: Q & runtime.Types.Extensions.UserArgs['query'] }
| { client?: C & runtime.Types.Extensions.UserArgs['client'] },
) => PrismaClientExtends<Args & ExtArgs> & Args['client'])
$transaction<R>(
fn: (prisma: Omit<this, runtime.ITXClientDenyList>) => Promise<R>,
options?: { maxWait?: number; timeout?: number; isolationLevel?: string },
): Promise<R>
$transaction<P extends Prisma.PrismaPromise<any>[]>(
arg: [...P],
options?: { isolationLevel?: string },
): Promise<runtime.Types.Utils.UnwrapTuple<P>>
}
export declare const dmmf: any
export declare type dmmf = any
/**
* Get the type of the value, that the Promise holds.
*/
export declare type PromiseType<T extends PromiseLike<any>> = T extends PromiseLike<infer U> ? U : T
/**
* Get the return type of a function which returns a Promise.
*/
export declare type PromiseReturnType<T extends (...args: any) => Promise<any>> = PromiseType<ReturnType<T>>
export namespace Prisma {
export type TransactionClient = any
export function defineExtension<
R extends runtime.Types.Extensions.UserArgs['result'] = {},
M extends runtime.Types.Extensions.UserArgs['model'] = {},
Q extends runtime.Types.Extensions.UserArgs['query'] = {},
C extends runtime.Types.Extensions.UserArgs['client'] = {},
Args extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.InternalArgs<R, M, {}, C>,
>(
args:
| ((client: PrismaClientExtends) => { $extends: { extArgs: Args } })
| { name?: string }
| { result?: R & runtime.Types.Extensions.UserArgs['result'] }
| { model?: M & runtime.Types.Extensions.UserArgs['model'] }
| { query?: Q & runtime.Types.Extensions.UserArgs['query'] }
| { client?: C & runtime.Types.Extensions.UserArgs['client'] },
): (client: any) => PrismaClientExtends<Args>
export type Extension = runtime.Types.Extensions.UserArgs
export import getExtensionContext = runtime.Extensions.getExtensionContext
export import Args = runtime.Types.Public.Args
export import Payload = runtime.Types.Public.Payload
export import Result = runtime.Types.Public.Result
export import Exact = runtime.Types.Public.Exact
export import PrismaPromise = runtime.Types.Public.PrismaPromise
export const prismaVersion: {
client: string
engine: string
}
}
+65
View File
@@ -0,0 +1,65 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/scripts/default-index.ts
var default_index_exports = {};
__export(default_index_exports, {
Prisma: () => Prisma,
PrismaClient: () => PrismaClient,
default: () => default_index_default
});
module.exports = __toCommonJS(default_index_exports);
// ../../node_modules/.pnpm/@prisma+engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2/node_modules/@prisma/engines-version/package.json
var prisma = {
enginesVersion: "605197351a3c8bdd595af2d2a9bc3025bca48ea2"
};
// package.json
var version = "5.22.0";
// src/runtime/utils/clientVersion.ts
var clientVersion = version;
// src/scripts/default-index.ts
var PrismaClient = class {
constructor() {
throw new Error('@prisma/client did not initialize yet. Please run "prisma generate" and try to import it again.');
}
};
function defineExtension(ext) {
if (typeof ext === "function") {
return ext;
}
return (client) => client.$extends(ext);
}
function getExtensionContext(that) {
return that;
}
var Prisma = {
defineExtension,
getExtensionContext,
prismaVersion: { client: clientVersion, engine: prisma.enginesVersion }
};
var default_index_default = { Prisma };
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Prisma,
PrismaClient
});
+5
View File
@@ -0,0 +1,5 @@
export function getPostInstallTrigger(): string
export const UNABLE_TO_FIND_POSTINSTALL_TRIGGER__EMPTY_STRING
export const UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING
export const UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR
export const UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR
+410
View File
@@ -0,0 +1,410 @@
// @ts-check
const childProcess = require('child_process')
const { promisify } = require('util')
const fs = require('fs')
const path = require('path')
const c = require('./colors')
const exec = promisify(childProcess.exec)
function debug(message, ...optionalParams) {
if (process.env.DEBUG && process.env.DEBUG === 'prisma:postinstall') {
console.log(message, ...optionalParams)
}
}
/**
* Adds `package.json` to the end of a path if it doesn't already exist'
* @param {string} pth
*/
function addPackageJSON(pth) {
if (pth.endsWith('package.json')) return pth
return path.join(pth, 'package.json')
}
/**
* Looks up for a `package.json` which is not `@prisma/cli` or `prisma` and returns the directory of the package
* @param {string | null} startPath - Path to Start At
* @param {number} limit - Find Up limit
* @returns {string | null}
*/
function findPackageRoot(startPath, limit = 10) {
if (!startPath || !fs.existsSync(startPath)) return null
let currentPath = startPath
// Limit traversal
for (let i = 0; i < limit; i++) {
const pkgPath = addPackageJSON(currentPath)
if (fs.existsSync(pkgPath)) {
try {
const pkg = require(pkgPath)
if (pkg.name && !['@prisma/cli', 'prisma'].includes(pkg.name)) {
return pkgPath.replace('package.json', '')
}
} catch {} // eslint-disable-line no-empty
}
currentPath = path.join(currentPath, '../')
}
return null
}
/**
* The `postinstall` hook of client sets up the ground and env vars for the `prisma generate` command,
* and runs it, showing a warning if the schema is not found.
* - initializes the ./node_modules/.prisma/client folder with the default index(-browser).js/index.d.ts,
* which define a `PrismaClient` class stub that throws an error if instantiated before the `prisma generate`
* command is successfully executed.
* - sets the path of the root of the project (TODO: to verify) to the `process.env.PRISMA_GENERATE_IN_POSTINSTALL`
* variable, or `'true'` if the project root cannot be found.
* - runs `prisma generate`, passing through additional information about the command that triggered the generation,
* which is useful for debugging/telemetry. It tries to use the local `prisma` package if it is installed, otherwise it
* falls back to the global `prisma` package. If neither options are available, it warns the user to install `prisma` first.
*/
async function main() {
if (process.env.INIT_CWD) {
process.chdir(process.env.INIT_CWD) // necessary, because npm chooses __dirname as process.cwd()
// in the postinstall hook
}
await createDefaultGeneratedThrowFiles()
// TODO: consider using the `which` package
const localPath = getLocalPackagePath()
// Only execute if !localpath
const installedGlobally = localPath ? undefined : await isInstalledGlobally()
// this is needed, so we can find the correct schemas in yarn workspace projects
const root = findPackageRoot(localPath)
process.env.PRISMA_GENERATE_IN_POSTINSTALL = root ? root : 'true'
debug({
localPath,
installedGlobally,
init_cwd: process.env.INIT_CWD,
PRISMA_GENERATE_IN_POSTINSTALL: process.env.PRISMA_GENERATE_IN_POSTINSTALL,
})
try {
if (localPath) {
await run('node', [localPath, 'generate', '--postinstall', doubleQuote(getPostInstallTrigger())])
return
}
if (installedGlobally) {
await run('prisma', ['generate', '--postinstall', doubleQuote(getPostInstallTrigger())])
return
}
} catch (e) {
// if exit code = 1 do not print
if (e && e !== 1) {
console.error(e)
}
debug(e)
}
if (!localPath && !installedGlobally) {
console.error(
`${c.yellow(
'warning',
)} In order to use "@prisma/client", please install Prisma CLI. You can install it with "npm add -D prisma".`,
)
}
}
function getLocalPackagePath() {
try {
const packagePath = require.resolve('prisma/package.json')
if (packagePath) {
return require.resolve('prisma')
}
} catch (e) {} // eslint-disable-line no-empty
// TODO: consider removing this
try {
const packagePath = require.resolve('@prisma/cli/package.json')
if (packagePath) {
return require.resolve('@prisma/cli')
}
} catch (e) {} // eslint-disable-line no-empty
return null
}
async function isInstalledGlobally() {
try {
const result = await exec('prisma -v')
if (result.stdout.includes('@prisma/client')) {
return true
} else {
console.error(`${c.yellow('warning')} You still have the ${c.bold('prisma')} cli (Prisma 1) installed globally.
Please uninstall it with either ${c.green('npm remove -g prisma')} or ${c.green('yarn global remove prisma')}.`)
}
} catch (e) {
return false
}
}
if (!process.env.PRISMA_SKIP_POSTINSTALL_GENERATE) {
main()
.catch((e) => {
if (e.stderr) {
if (e.stderr.includes(`Can't find schema.prisma`)) {
console.error(
`${c.yellow('warning')} @prisma/client needs a ${c.bold('schema.prisma')} to function, but couldn't find it.
Please either create one manually or use ${c.bold('prisma init')}.
Once you created it, run ${c.bold('prisma generate')}.
To keep Prisma related things separate, we recommend creating it in a subfolder called ${c.underline(
'./prisma',
)} like so: ${c.underline('./prisma/schema.prisma')}\n`,
)
} else {
console.error(e.stderr)
}
} else {
console.error(e)
}
process.exit(0)
})
.finally(() => {
debug(`postinstall trigger: ${getPostInstallTrigger()}`)
})
}
function run(cmd, params, cwd = process.cwd()) {
const child = childProcess.spawn(cmd, params, {
stdio: ['pipe', 'inherit', 'inherit'],
cwd,
})
return new Promise((resolve, reject) => {
child.on('close', () => {
resolve(undefined)
})
child.on('exit', (code) => {
if (code === 0) {
resolve(undefined)
} else {
reject(code)
}
})
child.on('error', () => {
reject()
})
})
}
/**
* Copies our default "throw" files into the default generation folder. These
* files are dummy and informative because they just throw an error to let the
* user know that they have forgotten to run `prisma generate` or that they
* don't have a a schema file yet. We only add these files at the default
* location `node_modules/.prisma/client`.
*/
async function createDefaultGeneratedThrowFiles() {
try {
const dotPrismaClientDir = path.join(__dirname, '../../../.prisma/client')
const denoPrismaClientDir = path.join(__dirname, '../../../.prisma/client/deno')
await makeDir(dotPrismaClientDir)
await makeDir(denoPrismaClientDir)
const defaultFileConfig = {
js: path.join(__dirname, 'default-index.js'),
ts: path.join(__dirname, 'default-index.d.ts'),
}
/**
* @type {Record<string, { js?: string; ts?: string } | undefined>}
*/
const defaultFiles = {
index: defaultFileConfig,
edge: defaultFileConfig,
default: defaultFileConfig,
wasm: defaultFileConfig,
'index-browser': {
js: path.join(__dirname, 'default-index.js'),
ts: undefined,
},
'deno/edge': {
js: undefined,
ts: path.join(__dirname, 'default-deno-edge.ts'),
},
}
for (const file of Object.keys(defaultFiles)) {
const { js, ts } = defaultFiles[file] ?? {}
const dotPrismaJsFilePath = path.join(dotPrismaClientDir, `${file}.js`)
const dotPrismaTsFilePath = path.join(dotPrismaClientDir, `${file}.d.ts`)
if (js && !fs.existsSync(dotPrismaJsFilePath) && fs.existsSync(js)) {
await fs.promises.copyFile(js, dotPrismaJsFilePath)
}
if (ts && !fs.existsSync(dotPrismaTsFilePath) && fs.existsSync(ts)) {
await fs.promises.copyFile(ts, dotPrismaTsFilePath)
}
}
} catch (e) {
console.error(e)
}
}
// TODO: can this be replaced some utility eg. mkdir
function makeDir(input) {
const make = async (pth) => {
try {
await fs.promises.mkdir(pth)
return pth
} catch (error) {
if (error.code === 'EPERM') {
throw error
}
if (error.code === 'ENOENT') {
if (path.dirname(pth) === pth) {
throw new Error(`operation not permitted, mkdir '${pth}'`)
}
if (error.message.includes('null bytes')) {
throw error
}
await make(path.dirname(pth))
return make(pth)
}
try {
const stats = await fs.promises.stat(pth)
if (!stats.isDirectory()) {
throw new Error('The path is not a directory')
}
} catch (_) {
throw error
}
return pth
}
}
return make(path.resolve(input))
}
/**
* Get the command that triggered this postinstall script being run. If there is
* an error while attempting to get this value then the string constant
* 'ERROR_WHILE_FINDING_POSTINSTALL_TRIGGER' is returned.
* This information is just necessary for telemetry.
* This is passed to `prisma generate` as a string like `--postinstall value`.
*/
function getPostInstallTrigger() {
/*
npm_config_argv` is not officially documented so here are our research notes
`npm_config_argv` is available to the postinstall script when the containing package has been installed by npm into some project.
An example of its value:
```
npm_config_argv: '{"remain":["../test"],"cooked":["add","../test"],"original":["add","../test"]}',
```
We are interesting in the data contained in the "original" field.
Trivia/Note: `npm_config_argv` is not available when running e.g. `npm install` on the containing package itself (e.g. when working on it)
Yarn mimics this data and environment variable. Here is an example following `yarn add` for the same package:
```
npm_config_argv: '{"remain":[],"cooked":["add"],"original":["add","../test"]}'
```
Other package managers like `pnpm` have not been tested.
*/
const maybe_npm_config_argv_string = process.env.npm_config_argv
if (maybe_npm_config_argv_string === undefined) {
return UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING
}
let npm_config_argv
try {
npm_config_argv = JSON.parse(maybe_npm_config_argv_string)
} catch (e) {
return `${UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR}: ${maybe_npm_config_argv_string}`
}
if (typeof npm_config_argv !== 'object' || npm_config_argv === null) {
return `${UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR}: ${maybe_npm_config_argv_string}`
}
const npm_config_argv_original_arr = npm_config_argv.original
if (!Array.isArray(npm_config_argv_original_arr)) {
return `${UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR}: ${maybe_npm_config_argv_string}`
}
const npm_config_argv_original = npm_config_argv_original_arr.filter((arg) => arg !== '').join(' ')
const command =
npm_config_argv_original === ''
? getPackageManagerName()
: [getPackageManagerName(), npm_config_argv_original].join(' ')
return command
}
/**
* Wrap double quotes around the given string.
*/
function doubleQuote(x) {
return `"${x}"`
}
/**
* Get the package manager name currently being used. If parsing fails, then the following pattern is returned:
* UNKNOWN_NPM_CONFIG_USER_AGENT(<string received>).
*/
function getPackageManagerName() {
const userAgent = process.env.npm_config_user_agent
if (!userAgent) return 'MISSING_NPM_CONFIG_USER_AGENT'
const name = parsePackageManagerName(userAgent)
if (!name) return `UNKNOWN_NPM_CONFIG_USER_AGENT(${userAgent})`
return name
}
/**
* Parse package manager name from useragent. If parsing fails, `null` is returned.
*/
function parsePackageManagerName(userAgent) {
let packageManager = null
// example: 'yarn/1.22.4 npm/? node/v13.11.0 darwin x64'
// References:
// - https://pnpm.io/only-allow-pnpm
// - https://github.com/cameronhunter/npm-config-user-agent-parser
if (userAgent) {
const matchResult = userAgent.match(/^([^/]+)\/.+/)
if (matchResult) {
packageManager = matchResult[1].trim()
}
}
return packageManager
}
// prettier-ignore
const UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING = 'UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING'
// prettier-ignore
const UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR = 'UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR'
// prettier-ignore
const UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR = 'UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR'
// expose for testing
exports.UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING = UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING
exports.UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR = UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR
exports.UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR = UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR
exports.getPostInstallTrigger = getPostInstallTrigger
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/sql'
+4
View File
@@ -0,0 +1,4 @@
'use strict'
module.exports = {
...require('.prisma/client/sql'),
}
+1
View File
@@ -0,0 +1 @@
export * from '../../.prisma/client/sql/index.mjs'
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/wasm'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('.prisma/client/wasm'),
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+29
View File
@@ -0,0 +1,29 @@
# @prisma/debug
A cached [`debug`](https://github.com/visionmedia/debug/).
---
⚠️ **Warning**: This package is intended for Prisma's internal use.
Its release cycle does not follow SemVer, which means we might release breaking changes (change APIs, remove functionality) without any prior warning.
If you are using this package, it would be helpful if you could help us gain an understanding where, how and why you are using it. Your feedback will be valuable to us to define a better API. Please share this information at https://github.com/prisma/prisma/discussions/13877 - Thanks!
## Usage
```ts
import Debug, { getLogs } from '@prisma/debug'
const debug = Debug('my-namespace')
try {
debug('hello')
debug(process.env)
throw new Error('oops')
} catch (e) {
console.error(e)
console.error(`We just crashed. But no worries, here are the debug logs:`)
// get 10 last lines of debug logs
console.error(getLogs(10))
}
```
+35
View File
@@ -0,0 +1,35 @@
/**
* Create a new debug instance with the given namespace.
*
* @example
* ```ts
* import Debug from '@prisma/debug'
* const debug = Debug('prisma:client')
* debug('Hello World')
* ```
*/
declare function debugCreate(namespace: string): ((...args: any[]) => void) & {
color: string;
enabled: boolean;
namespace: string;
log: (...args: string[]) => void;
extend: () => void;
};
declare const Debug: typeof debugCreate & {
enable(namespace: any): void;
disable(): any;
enabled(namespace: string): boolean;
log: (...args: string[]) => void;
formatters: {};
};
/**
* We can get the logs for all the last {@link MAX_ARGS_HISTORY} ${@link debugCall} that
* have happened in the different packages. Useful to generate error report links.
* @see https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
* @param numChars
* @returns
*/
export declare function getLogs(numChars?: number): string;
export declare function clearLogs(): void;
export { Debug };
export default Debug;
+236
View File
@@ -0,0 +1,236 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
Debug: () => Debug,
clearLogs: () => clearLogs,
default: () => src_default,
getLogs: () => getLogs
});
module.exports = __toCommonJS(src_exports);
// ../../node_modules/.pnpm/kleur@4.1.5/node_modules/kleur/colors.mjs
var colors_exports = {};
__export(colors_exports, {
$: () => $,
bgBlack: () => bgBlack,
bgBlue: () => bgBlue,
bgCyan: () => bgCyan,
bgGreen: () => bgGreen,
bgMagenta: () => bgMagenta,
bgRed: () => bgRed,
bgWhite: () => bgWhite,
bgYellow: () => bgYellow,
black: () => black,
blue: () => blue,
bold: () => bold,
cyan: () => cyan,
dim: () => dim,
gray: () => gray,
green: () => green,
grey: () => grey,
hidden: () => hidden,
inverse: () => inverse,
italic: () => italic,
magenta: () => magenta,
red: () => red,
reset: () => reset,
strikethrough: () => strikethrough,
underline: () => underline,
white: () => white,
yellow: () => yellow
});
var FORCE_COLOR;
var NODE_DISABLE_COLORS;
var NO_COLOR;
var TERM;
var isTTY = true;
if (typeof process !== "undefined") {
({ FORCE_COLOR, NODE_DISABLE_COLORS, NO_COLOR, TERM } = process.env || {});
isTTY = process.stdout && process.stdout.isTTY;
}
var $ = {
enabled: !NODE_DISABLE_COLORS && NO_COLOR == null && TERM !== "dumb" && (FORCE_COLOR != null && FORCE_COLOR !== "0" || isTTY)
};
function init(x, y) {
let rgx = new RegExp(`\\x1b\\[${y}m`, "g");
let open = `\x1B[${x}m`, close = `\x1B[${y}m`;
return function(txt) {
if (!$.enabled || txt == null) return txt;
return open + (!!~("" + txt).indexOf(close) ? txt.replace(rgx, close + open) : txt) + close;
};
}
var reset = init(0, 0);
var bold = init(1, 22);
var dim = init(2, 22);
var italic = init(3, 23);
var underline = init(4, 24);
var inverse = init(7, 27);
var hidden = init(8, 28);
var strikethrough = init(9, 29);
var black = init(30, 39);
var red = init(31, 39);
var green = init(32, 39);
var yellow = init(33, 39);
var blue = init(34, 39);
var magenta = init(35, 39);
var cyan = init(36, 39);
var white = init(37, 39);
var gray = init(90, 39);
var grey = init(90, 39);
var bgBlack = init(40, 49);
var bgRed = init(41, 49);
var bgGreen = init(42, 49);
var bgYellow = init(43, 49);
var bgBlue = init(44, 49);
var bgMagenta = init(45, 49);
var bgCyan = init(46, 49);
var bgWhite = init(47, 49);
// src/index.ts
var MAX_ARGS_HISTORY = 100;
var COLORS = ["green", "yellow", "blue", "magenta", "cyan", "red"];
var argsHistory = [];
var lastTimestamp = Date.now();
var lastColor = 0;
var processEnv = typeof process !== "undefined" ? process.env : {};
globalThis.DEBUG ??= processEnv.DEBUG ?? "";
globalThis.DEBUG_COLORS ??= processEnv.DEBUG_COLORS ? processEnv.DEBUG_COLORS === "true" : true;
var topProps = {
enable(namespace) {
if (typeof namespace === "string") {
globalThis.DEBUG = namespace;
}
},
disable() {
const prev = globalThis.DEBUG;
globalThis.DEBUG = "";
return prev;
},
// this is the core logic to check if logging should happen or not
enabled(namespace) {
const listenedNamespaces = globalThis.DEBUG.split(",").map((s) => {
return s.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
});
const isListened = listenedNamespaces.some((listenedNamespace) => {
if (listenedNamespace === "" || listenedNamespace[0] === "-") return false;
return namespace.match(RegExp(listenedNamespace.split("*").join(".*") + "$"));
});
const isExcluded = listenedNamespaces.some((listenedNamespace) => {
if (listenedNamespace === "" || listenedNamespace[0] !== "-") return false;
return namespace.match(RegExp(listenedNamespace.slice(1).split("*").join(".*") + "$"));
});
return isListened && !isExcluded;
},
log: (...args) => {
const [namespace, format, ...rest] = args;
const logWithFormatting = console.warn ?? console.log;
logWithFormatting(`${namespace} ${format}`, ...rest);
},
formatters: {}
// not implemented
};
function debugCreate(namespace) {
const instanceProps = {
color: COLORS[lastColor++ % COLORS.length],
enabled: topProps.enabled(namespace),
namespace,
log: topProps.log,
extend: () => {
}
// not implemented
};
const debugCall = (...args) => {
const { enabled, namespace: namespace2, color, log } = instanceProps;
if (args.length !== 0) {
argsHistory.push([namespace2, ...args]);
}
if (argsHistory.length > MAX_ARGS_HISTORY) {
argsHistory.shift();
}
if (topProps.enabled(namespace2) || enabled) {
const stringArgs = args.map((arg) => {
if (typeof arg === "string") {
return arg;
}
return safeStringify(arg);
});
const ms = `+${Date.now() - lastTimestamp}ms`;
lastTimestamp = Date.now();
if (globalThis.DEBUG_COLORS) {
log(colors_exports[color](bold(namespace2)), ...stringArgs, colors_exports[color](ms));
} else {
log(namespace2, ...stringArgs, ms);
}
}
};
return new Proxy(debugCall, {
get: (_, prop) => instanceProps[prop],
set: (_, prop, value) => instanceProps[prop] = value
});
}
var Debug = new Proxy(debugCreate, {
get: (_, prop) => topProps[prop],
set: (_, prop, value) => topProps[prop] = value
});
function safeStringify(value, indent = 2) {
const cache = /* @__PURE__ */ new Set();
return JSON.stringify(
value,
(key, value2) => {
if (typeof value2 === "object" && value2 !== null) {
if (cache.has(value2)) {
return `[Circular *]`;
}
cache.add(value2);
} else if (typeof value2 === "bigint") {
return value2.toString();
}
return value2;
},
indent
);
}
function getLogs(numChars = 7500) {
const logs = argsHistory.map(([namespace, ...args]) => {
return `${namespace} ${args.map((arg) => {
if (typeof arg === "string") {
return arg;
} else {
return JSON.stringify(arg);
}
}).join(" ")}`;
}).join("\n");
if (logs.length < numChars) {
return logs;
}
return logs.slice(-numChars);
}
function clearLogs() {
argsHistory.length = 0;
}
var src_default = Debug;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Debug,
clearLogs,
getLogs
});
+2
View File
@@ -0,0 +1,2 @@
export declare function removeISODate(str: string): string;
export declare function sanitizeTestLogs(str: string): string;
+74
View File
@@ -0,0 +1,74 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// ../../node_modules/.pnpm/ansi-regex@5.0.1/node_modules/ansi-regex/index.js
var require_ansi_regex = __commonJS({
"../../node_modules/.pnpm/ansi-regex@5.0.1/node_modules/ansi-regex/index.js"(exports2, module2) {
"use strict";
module2.exports = ({ onlyFirst = false } = {}) => {
const pattern = [
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"
].join("|");
return new RegExp(pattern, onlyFirst ? void 0 : "g");
};
}
});
// ../../node_modules/.pnpm/strip-ansi@6.0.1/node_modules/strip-ansi/index.js
var require_strip_ansi = __commonJS({
"../../node_modules/.pnpm/strip-ansi@6.0.1/node_modules/strip-ansi/index.js"(exports2, module2) {
"use strict";
var ansiRegex = require_ansi_regex();
module2.exports = (string) => typeof string === "string" ? string.replace(ansiRegex(), "") : string;
}
});
// src/util.ts
var util_exports = {};
__export(util_exports, {
removeISODate: () => removeISODate,
sanitizeTestLogs: () => sanitizeTestLogs
});
module.exports = __toCommonJS(util_exports);
var import_strip_ansi = __toESM(require_strip_ansi());
function removeISODate(str) {
return str.replace(/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/gim, "");
}
function sanitizeTestLogs(str) {
return (0, import_strip_ansi.default)(str).split("\n").map((l) => removeISODate(l.replace(/\+\d+ms$/, "")).trim()).join("\n");
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
removeISODate,
sanitizeTestLogs
});
+37
View File
@@ -0,0 +1,37 @@
{
"name": "@prisma/debug",
"version": "5.22.0",
"description": "This package is intended for Prisma's internal use",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "Apache-2.0",
"author": "Tim Suchanek <suchanek@prisma.io>",
"homepage": "https://www.prisma.io",
"repository": {
"type": "git",
"url": "https://github.com/prisma/prisma.git",
"directory": "packages/debug"
},
"bugs": "https://github.com/prisma/prisma/issues",
"devDependencies": {
"@types/jest": "29.5.12",
"@types/node": "18.19.31",
"esbuild": "0.23.0",
"jest": "29.7.0",
"jest-junit": "16.0.0",
"strip-ansi": "6.0.1",
"kleur": "4.1.5",
"typescript": "5.4.5"
},
"files": [
"README.md",
"dist"
],
"dependencies": {},
"sideEffects": false,
"scripts": {
"dev": "DEV=true tsx helpers/build.ts",
"build": "tsx helpers/build.ts",
"test": "jest"
}
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+8
View File
@@ -0,0 +1,8 @@
# `@prisma/engines-version`
This package exports the Prisma Engines version to be downloaded from Prisma CDN.
⚠️ **Warning**: This package is intended for Prisma's internal use.
Its release cycle does not follow SemVer, which means we might release breaking changes (change APIs, remove functionality) without any prior warning.
See its companion, [`@prisma/engines` npm package](https://www.npmjs.com/package/@prisma/engines).
+1
View File
@@ -0,0 +1 @@
export declare const enginesVersion: string;
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.enginesVersion = void 0;
exports.enginesVersion = require('./package.json').prisma.enginesVersion;
//# sourceMappingURL=index.js.map
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@prisma/engines-version",
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"main": "index.js",
"types": "index.d.ts",
"license": "Apache-2.0",
"author": "Tim Suchanek <suchanek@prisma.io>",
"prisma": {
"enginesVersion": "605197351a3c8bdd595af2d2a9bc3025bca48ea2"
},
"repository": {
"type": "git",
"url": "https://github.com/prisma/engines-wrapper.git",
"directory": "packages/engines-version"
},
"devDependencies": {
"@types/node": "18.19.34",
"typescript": "4.9.5"
},
"files": [
"index.js",
"index.d.ts"
],
"scripts": {
"build": "tsc -d"
}
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+13
View File
@@ -0,0 +1,13 @@
# `@prisma/engines`
⚠️ **Warning**: This package is intended for Prisma's internal use.
Its release cycle does not follow SemVer, which means we might release breaking changes (change APIs, remove functionality) without any prior warning.
The postinstall hook of this package downloads all Prisma engines available for the current platform, namely the Query Engine and the Schema Engine from the Prisma CDN.
The engines version to be downloaded is directly determined by the version of its `@prisma/engines-version` dependency.
You should probably not use this package directly, but instead use one of these:
- [`prisma` CLI](https://www.npmjs.com/package/prisma)
- [`@prisma/client`](https://www.npmjs.com/package/@prisma/client)
+18
View File
@@ -0,0 +1,18 @@
import { BinaryType as BinaryType_2 } from '@prisma/fetch-engine';
import { enginesVersion } from '@prisma/engines-version';
export declare const DEFAULT_CLI_QUERY_ENGINE_BINARY_TYPE = BinaryType.QueryEngineLibrary;
export { enginesVersion }
export declare function ensureBinariesExist(): Promise<void>;
/**
* Checks if the env override `PRISMA_CLI_QUERY_ENGINE_TYPE` is set to `library` or `binary`
* Otherwise returns the default
*/
export declare function getCliQueryEngineBinaryType(): BinaryType_2;
export declare function getEnginesPath(): string;
export { }

Some files were not shown because too many files have changed in this diff Show More