Compare commits
5
Commits
84cb0ec0f6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dc723477b | ||
|
|
729fc90365 | ||
|
|
44f54b4793 | ||
|
|
af6b7e01cd | ||
|
|
aa3480af17 |
@@ -12,6 +12,9 @@ VITE_SUPABASE_ANON_KEY=your-anon-key
|
|||||||
# Optional: for OAuth / magic link redirects
|
# Optional: for OAuth / magic link redirects
|
||||||
# VITE_SUPABASE_REDIRECT_URL=http://localhost:5173
|
# VITE_SUPABASE_REDIRECT_URL=http://localhost:5173
|
||||||
|
|
||||||
|
# Set to "true" to disable new user registration (login still works)
|
||||||
|
# VITE_DISABLE_REGISTRATION=false
|
||||||
|
|
||||||
# App uses schema "omotomo" for decks/questions. Ensure the API exposes it:
|
# App uses schema "omotomo" for decks/questions. Ensure the API exposes it:
|
||||||
# Self-hosted: add "omotomo" to PostgREST db-schemas (e.g. in config).
|
# Self-hosted: add "omotomo" to PostgREST db-schemas (e.g. in config).
|
||||||
# Cloud: Dashboard > Project Settings > API > Exposed schemas.
|
# Cloud: Dashboard > Project Settings > API > Exposed schemas.
|
||||||
|
|||||||
@@ -13,11 +13,20 @@ This starts both the Vite dev server (frontend) and the API server (default port
|
|||||||
|
|
||||||
Copy `.env.sample` to `.env` and set `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` for the Svelte app and (optionally) the API server.
|
Copy `.env.sample` to `.env` and set `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` for the Svelte app and (optionally) the API server.
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds the frontend to `dist/` and starts the server. The same process serves both the API and the built SPA at `http://localhost:3001` (or your `PORT`). Without a `dist/` folder (e.g. API-only), the server still runs and serves only the API.
|
||||||
|
|
||||||
## API server (REST API for mobile / other clients)
|
## API server (REST API for mobile / other clients)
|
||||||
|
|
||||||
The Express API uses the same Supabase backend. Optional env: `SUPABASE_URL`, `SUPABASE_ANON_KEY` (default to `VITE_*`), `PORT` (default `3001`).
|
The Express API uses the same Supabase backend. Optional env: `SUPABASE_URL`, `SUPABASE_ANON_KEY` (default to `VITE_*`), `PORT` (default `3001`).
|
||||||
|
|
||||||
- **Run**: `npm run dev:server` or `npm run start:api`
|
- **Run**: `npm run dev:server`, `npm run start`, or `npm run start:api`. When `dist/` exists, the server also serves the frontend.
|
||||||
- **Auth**: `POST /api/auth/login`, `POST /api/auth/register`, `GET /api/auth/session`, `GET /api/auth/profile` (Bearer token for protected routes; profile includes `display_name`, `email`, `avatar_url`)
|
- **Auth**: `POST /api/auth/login`, `POST /api/auth/register`, `GET /api/auth/session`, `GET /api/auth/profile` (Bearer token for protected routes; profile includes `display_name`, `email`, `avatar_url`)
|
||||||
- **Decks**: `GET /api/decks/mine`, `GET /api/decks/published`, `GET /api/decks/:id`, `POST /api/decks`, `PATCH /api/decks/:id`, `DELETE /api/decks/:id`, `POST /api/decks/:id/publish`, `POST /api/decks/:id/copy`, `GET /api/decks/:id/update-preview`, `POST /api/decks/:id/apply-update`
|
- **Decks**: `GET /api/decks/mine`, `GET /api/decks/published`, `GET /api/decks/:id`, `POST /api/decks`, `PATCH /api/decks/:id`, `DELETE /api/decks/:id`, `POST /api/decks/:id/publish`, `POST /api/decks/:id/copy`, `GET /api/decks/:id/update-preview`, `POST /api/decks/:id/apply-update`
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"dev:server": "node server/index.js",
|
"dev:server": "node server/index.js",
|
||||||
|
"start": "node server/index.js",
|
||||||
"start:api": "node server/index.js"
|
"start:api": "node server/index.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import * as decksApi from '../src/lib/api/decks-core.js';
|
import * as decksApi from '../src/lib/api/decks-core.js';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
|
const distPath = path.join(projectRoot, 'dist');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors({ origin: true, credentials: true }));
|
app.use(cors({ origin: true, credentials: true }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -358,7 +365,21 @@ app.delete('/api/decks/:id', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Serve built frontend when dist/ exists (production)
|
||||||
|
if (fs.existsSync(distPath)) {
|
||||||
|
app.use(express.static(distPath));
|
||||||
|
app.get('*', (req, res, next) => {
|
||||||
|
if (req.path.startsWith('/api')) return next();
|
||||||
|
res.sendFile(path.join(distPath, 'index.html'), (err) => {
|
||||||
|
if (err) next(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`API server listening on http://localhost:${PORT}`);
|
console.log(`API server listening on http://localhost:${PORT}`);
|
||||||
|
if (fs.existsSync(distPath)) {
|
||||||
|
console.log(`Serving frontend from dist/ (open http://localhost:${PORT})`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-9
@@ -10,6 +10,8 @@
|
|||||||
$: myDecksActive = loc === '/' || loc === '/decks/new' || /^\/decks\/[^/]+\/edit$/.test(loc) || $navContext === 'my-decks';
|
$: myDecksActive = loc === '/' || loc === '/decks/new' || /^\/decks\/[^/]+\/edit$/.test(loc) || $navContext === 'my-decks';
|
||||||
$: communityActive = loc === '/community' || loc.startsWith('/community/') || $navContext === 'community';
|
$: communityActive = loc === '/community' || loc.startsWith('/community/') || $navContext === 'community';
|
||||||
|
|
||||||
|
const registrationDisabled = import.meta.env.VITE_DISABLE_REGISTRATION === 'true';
|
||||||
|
|
||||||
let showPopup = false;
|
let showPopup = false;
|
||||||
let mode = 'login'; // 'login' | 'register'
|
let mode = 'login'; // 'login' | 'register'
|
||||||
let email = '';
|
let email = '';
|
||||||
@@ -206,14 +208,16 @@
|
|||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
</button>
|
</button>
|
||||||
<button
|
{#if !registrationDisabled}
|
||||||
type="button"
|
<button
|
||||||
class="tab"
|
type="button"
|
||||||
class:active={mode === 'register'}
|
class="tab"
|
||||||
onclick={() => { mode = 'register'; auth.clearError(); registerSuccess = false; }}
|
class:active={mode === 'register'}
|
||||||
>
|
onclick={() => { mode = 'register'; auth.clearError(); registerSuccess = false; }}
|
||||||
Register
|
>
|
||||||
</button>
|
Register
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if registerSuccess && !$auth.user}
|
{#if registerSuccess && !$auth.user}
|
||||||
@@ -250,7 +254,7 @@
|
|||||||
/>
|
/>
|
||||||
{#if $auth.error}
|
{#if $auth.error}
|
||||||
<p class="auth-error">{$auth.error}</p>
|
<p class="auth-error">{$auth.error}</p>
|
||||||
{#if mode === 'login'}
|
{#if mode === 'login' && !registrationDisabled}
|
||||||
<p class="auth-switch">
|
<p class="auth-switch">
|
||||||
Do you have an account? <button type="button" class="auth-link" onclick={() => { mode = 'register'; auth.clearError(); }}>Register here</button>.
|
Do you have an account? <button type="button" class="auth-link" onclick={() => { mode = 'register'; auth.clearError(); }}>Register here</button>.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ function createAuthStore() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
await supabase.auth.signOut();
|
|
||||||
set({ user: null, loading: false, error: null });
|
set({ user: null, loading: false, error: null });
|
||||||
|
await supabase.auth.signOut();
|
||||||
},
|
},
|
||||||
clearError: () => update((s) => ({ ...s, error: null })),
|
clearError: () => update((s) => ({ ...s, error: null })),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -130,6 +130,25 @@
|
|||||||
function cancel() {
|
function cancel() {
|
||||||
push('/');
|
push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let copied = false;
|
||||||
|
function copyJson() {
|
||||||
|
const payload = {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
config,
|
||||||
|
questions: questions.map((q) => ({
|
||||||
|
prompt: q.prompt,
|
||||||
|
explanation: q.explanation,
|
||||||
|
answers: q.answers,
|
||||||
|
correct_answer_indices: q.correct_answer_indices,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => {
|
||||||
|
copied = true;
|
||||||
|
setTimeout(() => { copied = false; }, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="page-header page-header-fixed">
|
<header class="page-header page-header-fixed">
|
||||||
@@ -166,7 +185,10 @@
|
|||||||
placeholder="Paste deck JSON (title, description, config, questions)"
|
placeholder="Paste deck JSON (title, description, config, questions)"
|
||||||
rows="8"
|
rows="8"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button type="button" class="btn btn-secondary" onclick={loadFromJson}>Load from JSON</button>
|
<div class="json-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick={loadFromJson}>Load from JSON</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick={copyJson}>{copied ? 'Copied!' : 'Copy JSON'}</button>
|
||||||
|
</div>
|
||||||
{#if jsonError}
|
{#if jsonError}
|
||||||
<p class="error">{jsonError}</p>
|
<p class="error">{jsonError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -291,6 +313,12 @@
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.json-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user