Skip to content

Commit 78f2b3a

Browse files
authored
Merge pull request #28 from DVDAGames/feat/revamp
Revamp from local-only to DB-backed for retaining state cross-device; allow users to create new Hex Flower Engines
2 parents 8abc56a + 245b3cd commit 78f2b3a

166 files changed

Lines changed: 21656 additions & 14874 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env

Lines changed: 0 additions & 2 deletions
This file was deleted.

.example.env

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Cloudflare D1 Database
2+
# Get this after running: npx wrangler d1 create hex-flower-engine-db
3+
# The command will output the database_id - copy it here and to wrangler.toml
4+
CLOUDFLARE_D1_DATABASE_ID=
5+
6+
# Cloudflare Account (for GitHub Actions deployment)
7+
# Find at: https://dash.cloudflare.com/ → right sidebar → Account ID
8+
CLOUDFLARE_ACCOUNT_ID=
9+
10+
# Cloudflare API Token (for GitHub Actions deployment)
11+
# Create at: https://dash.cloudflare.com/profile/api-tokens
12+
# Use template: "Edit Cloudflare Workers" or create custom with:
13+
# - Account: Cloudflare Pages:Edit, D1:Edit
14+
# - Zone: (optional, only if using custom domain)
15+
CLOUDFLARE_API_TOKEN=
16+
17+
# JWT Secret for signing auth tokens
18+
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
19+
JWT_SECRET=
20+
21+
# Resend API Key for sending magic link emails
22+
# Get from: https://resend.com/api-keys
23+
RESEND_API_KEY=
24+
25+
# Admin email addresses (comma-separated)
26+
# These users will have admin access to approve public gallery submissions
27+
ADMIN_EMAILS=your-email@example.com
28+
29+
# Sender email address for magic link emails
30+
# Format: "Display Name <email@yourdomain.com>"
31+
# Must be from a verified domain in Resend, or use onboarding@resend.dev for testing
32+
EMAIL_FROM=Hex Flower Engine <info@yourdomain.com>
33+
34+
# Environment (production or development)
35+
ENVIRONMENT=development

.github/workflows/main.yml

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,62 @@
1-
# This is a basic workflow to help you get started with Actions
1+
# Deploy Hex Flower Engine to Cloudflare Pages
22

3-
name: Publish to Github Pages
3+
name: Deploy to Cloudflare Pages
44

5-
# Controls when the action will run. Triggers the workflow on push or pull request
6-
# events but only for the master branch
75
on:
86
push:
9-
branches: [ master ]
7+
branches: [main]
8+
pull_request:
9+
branches: [main]
10+
workflow_dispatch:
11+
12+
concurrency:
13+
group: "cloudflare-pages-${{ github.ref }}"
14+
cancel-in-progress: true
1015

11-
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
1216
jobs:
13-
# This workflow contains a single job called "deploy"
14-
deploy:
15-
name: Deploy to gh-pages
16-
# The type of runner that the job will run onruns-on: ubuntu-latest
17+
build-and-deploy:
18+
name: Build and Deploy
1719
runs-on: ubuntu-latest
20+
permissions:
21+
contents: read
22+
deployments: write
1823
steps:
19-
- uses: actions/checkout@v2
20-
- uses: borales/actions-yarn@v2.0.0
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Setup Node.js
28+
uses: actions/setup-node@v4
2129
with:
22-
cmd: install # will run `yarn install` command
23-
- uses: borales/actions-yarn@v2.0.0
30+
node-version: "20"
31+
cache: "npm"
32+
33+
- name: Install dependencies
34+
run: npm ci
35+
36+
- name: Type check
37+
run: npm run type-check
38+
39+
- name: Build
40+
run: npm run build
41+
42+
# Only run migrations and deploy on push to main (not PRs)
43+
- name: Run D1 Migrations
44+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
45+
uses: cloudflare/wrangler-action@v3
46+
with:
47+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
48+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
49+
command: d1 migrations apply hex-flower-engine-db --remote
50+
51+
- name: Deploy to Cloudflare Pages
52+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
53+
uses: cloudflare/wrangler-action@v3
2454
with:
25-
cmd: deploy # will run `yarn build` command
26-
- name: Success
27-
run: echo "Successfully deployed to gh-pages branch"
55+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
56+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
57+
command: pages deploy dist --project-name=hex-flower-engine
58+
59+
# For PRs, just report build success
60+
- name: Build Success
61+
if: github.event_name == 'pull_request'
62+
run: echo "✅ Build and type check passed for PR"

.gitignore

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,23 @@
1010

1111
# production
1212
/build
13+
/dist
1314

14-
# misc
15+
# env
1516
.DS_Store
17+
.env
1618
.env.local
1719
.env.development.local
1820
.env.test.local
1921
.env.production.local
2022

23+
# misc
24+
/.tmp
25+
26+
# wrangler
27+
.dev.vars
28+
.wrangler
29+
2130
npm-debug.log*
2231
yarn-debug.log*
2332
yarn-error.log*

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24.13.0

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1-
# React Hex Flower Engine
1+
# Hex v2
22

3-
![Weather Generation Hex Flower Engine](./public/weather-flower-demo.png)
3+
![Hex v2](./public/hex-v2.png)
44

5-
This is a simple application for managing a
6-
[Hex Flower Engine](https://goblinshenchman.wordpress.com/2018/10/25/2d6-hex-power-flower/)
5+
Hex is an application for managing
6+
[Hex Flower Engines](https://goblinshenchman.wordpress.com/2018/10/25/2d6-hex-power-flower/)
77
for any tabletop game in the browser.
88

9+
## Create and Share Hex Flower Engines
10+
11+
In the Hex application, you can create your own Hex Flower Engines, share them with your players, and even publish them to the Garden, where other users can find them and use them in their own games.
12+
13+
The Hex Editor allows you to choose a label, icon, color, and description for each Hex cell, and also customize the Hex Flower Engine movement rules for each roll outcome for that cell.
14+
15+
**Coming Soon**:
16+
17+
- Fork published engines and save your own version of them with modifications
18+
- Use movement rules other than `2d6` and `1d19`
19+
- Offline -> Online sync of engine state and editor state in case you lose network connection while using the application
20+
21+
### Legacy Version
22+
23+
The legacy version of this application is still hosted on GitHub Pages, but `v2` is a much better application for your needs and provides an interface for you to configure your own Hex Flower Engines.
24+
925
It currently supports two engines:
1026

1127
- `Standard Hex Flower Engine`: basic hex flower engine with standard movement rules

functions/api/admin/history.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Admin API: Get review history (approved/rejected engines)
2+
// GET /api/admin/history
3+
4+
import type { Env } from '../../types';
5+
import { getAuthUser, isAdminEmail, json, errorResponse } from '../../utils';
6+
7+
interface EngineRow {
8+
id: string;
9+
owner_id: string;
10+
definition: string;
11+
visibility: string;
12+
submitted_for_review_at: string;
13+
reviewed_at: string;
14+
reviewed_by: string;
15+
rejection_reason: string | null;
16+
use_count: number;
17+
created_at: string;
18+
updated_at: string;
19+
user_email: string;
20+
user_display_name: string | null;
21+
reviewer_email: string | null;
22+
reviewer_display_name: string | null;
23+
}
24+
25+
export const onRequestGet: PagesFunction<Env> = async (context) => {
26+
const { request, env } = context;
27+
28+
try {
29+
// Verify admin authentication
30+
const session = await getAuthUser(request, env);
31+
if (!session) {
32+
return errorResponse('Unauthorized', 401);
33+
}
34+
35+
if (!isAdminEmail(session.email, env)) {
36+
return errorResponse('Forbidden - Admin access required', 403);
37+
}
38+
39+
// Get query params for filtering
40+
const url = new URL(request.url);
41+
const filter = url.searchParams.get('filter') || 'all'; // 'all', 'approved', 'rejected'
42+
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 100);
43+
44+
// Build visibility filter
45+
let visibilityFilter = "e.visibility IN ('public', 'private')";
46+
if (filter === 'approved') {
47+
visibilityFilter = "e.visibility = 'public'";
48+
} else if (filter === 'rejected') {
49+
visibilityFilter = "e.visibility = 'private'";
50+
}
51+
52+
// Get reviewed engines with user and reviewer info
53+
const result = await env.DB.prepare(`
54+
SELECT
55+
e.id,
56+
e.owner_id,
57+
e.definition,
58+
e.visibility,
59+
e.submitted_for_review_at,
60+
e.reviewed_at,
61+
e.reviewed_by,
62+
e.rejection_reason,
63+
e.use_count,
64+
e.created_at,
65+
e.updated_at,
66+
p.email as user_email,
67+
p.display_name as user_display_name,
68+
r.email as reviewer_email,
69+
r.display_name as reviewer_display_name
70+
FROM engines e
71+
JOIN profiles p ON e.owner_id = p.id
72+
LEFT JOIN profiles r ON e.reviewed_by = r.id
73+
WHERE e.reviewed_at IS NOT NULL
74+
AND ${visibilityFilter}
75+
ORDER BY e.reviewed_at DESC
76+
LIMIT ?
77+
`).bind(limit).all();
78+
79+
const engines = (result.results as unknown as EngineRow[]).map((row) => ({
80+
id: row.id,
81+
ownerId: row.owner_id,
82+
definition: JSON.parse(row.definition),
83+
visibility: row.visibility,
84+
submittedAt: row.submitted_for_review_at,
85+
reviewedAt: row.reviewed_at,
86+
rejectionReason: row.rejection_reason,
87+
useCount: row.use_count,
88+
createdAt: row.created_at,
89+
updatedAt: row.updated_at,
90+
user: {
91+
email: row.user_email,
92+
displayName: row.user_display_name,
93+
},
94+
reviewer: row.reviewer_email ? {
95+
email: row.reviewer_email,
96+
displayName: row.reviewer_display_name,
97+
} : null,
98+
}));
99+
100+
return json({ engines });
101+
} catch (error) {
102+
console.error('Admin history error:', error);
103+
return errorResponse('Failed to fetch review history', 500);
104+
}
105+
};

functions/api/admin/pending.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Admin API: Get pending engines for review
2+
// GET /api/admin/pending
3+
4+
import type { Env } from '../../types';
5+
import { getAuthUser, isAdminEmail, json, errorResponse } from '../../utils';
6+
7+
interface EngineRow {
8+
id: string;
9+
owner_id: string;
10+
definition: string;
11+
visibility: string;
12+
submitted_at: string;
13+
use_count: number;
14+
created_at: string;
15+
updated_at: string;
16+
user_email: string;
17+
user_display_name: string | null;
18+
}
19+
20+
export const onRequestGet: PagesFunction<Env> = async (context) => {
21+
const { request, env } = context;
22+
23+
try {
24+
// Verify admin authentication
25+
const session = await getAuthUser(request, env);
26+
if (!session) {
27+
return errorResponse('Unauthorized', 401);
28+
}
29+
30+
if (!isAdminEmail(session.email, env)) {
31+
return errorResponse('Forbidden - Admin access required', 403);
32+
}
33+
34+
// Get all pending engines with user info
35+
const result = await env.DB.prepare(`
36+
SELECT
37+
e.id,
38+
e.owner_id,
39+
e.definition,
40+
e.visibility,
41+
e.submitted_for_review_at as submitted_at,
42+
e.use_count,
43+
e.created_at,
44+
e.updated_at,
45+
p.email as user_email,
46+
p.display_name as user_display_name
47+
FROM engines e
48+
JOIN profiles p ON e.owner_id = p.id
49+
WHERE e.visibility = 'pending_review'
50+
ORDER BY e.submitted_for_review_at ASC
51+
`).all();
52+
53+
const engines = (result.results as unknown as EngineRow[]).map((row) => ({
54+
id: row.id,
55+
ownerId: row.owner_id,
56+
definition: JSON.parse(row.definition),
57+
visibility: row.visibility,
58+
submittedAt: row.submitted_at,
59+
useCount: row.use_count,
60+
createdAt: row.created_at,
61+
updatedAt: row.updated_at,
62+
user: {
63+
email: row.user_email,
64+
displayName: row.user_display_name,
65+
},
66+
}));
67+
68+
return json({ engines });
69+
} catch (error) {
70+
console.error('Admin pending error:', error);
71+
return errorResponse('Failed to fetch pending engines', 500);
72+
}
73+
};

0 commit comments

Comments
 (0)