From be845bb15442363cbc4f35300efec34036d92551 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Tue, 23 Jun 2026 20:17:36 +0700 Subject: [PATCH] Dashboard: ported over dashboard to monorepo --- .../apps/artcraft-dashboard/.claude/CLAUDE.md | 60 + .../.claude/hooks/vexp-guard.sh | 14 + .../artcraft-dashboard/.claude/settings.json | 16 + .../.claude/settings.local.json | 9 + .../artcraft-dashboard/.vscode/settings.json | 23 + frontend/apps/artcraft-dashboard/README.md | 2 + frontend/apps/artcraft-dashboard/_redirects | 41 + .../apps/artcraft-dashboard/components.json | 24 + .../apps/artcraft-dashboard/eslint.config.js | 23 + frontend/apps/artcraft-dashboard/index.html | 19 + frontend/apps/artcraft-dashboard/netlify.toml | 15 + frontend/apps/artcraft-dashboard/package.json | 42 + .../apps/artcraft-dashboard/public/_redirects | 2 + .../public/artcraft-icon.svg | 13 + .../script/netlify_build.sh | 17 + frontend/apps/artcraft-dashboard/src/App.css | 42 + frontend/apps/artcraft-dashboard/src/App.tsx | 103 + .../artcraft-dashboard/src/api/ApiManager.ts | 124 + .../artcraft-dashboard/src/api/MediaApi.ts | 305 + .../src/api/ModerationApi.ts | 798 ++ .../artcraft-dashboard/src/api/UsersApi.ts | 299 + .../src/app/dashboard/data.json | 614 + .../src/assets/artcraft-icon.svg | 13 + .../artcraft-dashboard/src/assets/react.svg | 1 + .../src/components/BanUserDialog.tsx | 216 + .../src/components/ChangeUserEmailDialog.tsx | 207 + .../src/components/GiveCreditsDialog.tsx | 174 + .../src/components/ImpersonateUserDialog.tsx | 163 + .../src/components/JobsTable.tsx | 298 + .../src/components/MediaCard.tsx | 214 + .../src/components/MediaDetailModal.tsx | 457 + .../src/components/RawTag.tsx | 22 + .../src/components/TokenSearchForm.tsx | 78 + .../src/components/UserFeatureFlagsDialog.tsx | 378 + .../src/components/UserLookupPanel.tsx | 157 + .../src/components/app-sidebar.tsx | 239 + .../src/components/chart-area-interactive.tsx | 291 + .../src/components/data-table.tsx | 805 ++ .../src/components/nav-documents.tsx | 92 + .../src/components/nav-main.tsx | 54 + .../src/components/nav-secondary.tsx | 62 + .../src/components/nav-user.tsx | 112 + .../src/components/section-cards.tsx | 102 + .../src/components/settings-dialog.tsx | 81 + .../src/components/site-header.tsx | 30 + .../src/components/theme-provider.tsx | 6 + .../src/components/ui/alert-dialog.tsx | 196 + .../src/components/ui/alert.tsx | 66 + .../src/components/ui/avatar.tsx | 107 + .../src/components/ui/badge.tsx | 48 + .../src/components/ui/breadcrumb.tsx | 109 + .../src/components/ui/button.tsx | 64 + .../src/components/ui/card.tsx | 92 + .../src/components/ui/chart.tsx | 355 + .../src/components/ui/checkbox.tsx | 30 + .../src/components/ui/dialog.tsx | 158 + .../src/components/ui/drawer.tsx | 133 + .../src/components/ui/dropdown-menu.tsx | 257 + .../src/components/ui/field.tsx | 246 + .../src/components/ui/input.tsx | 21 + .../src/components/ui/label.tsx | 24 + .../src/components/ui/select.tsx | 188 + .../src/components/ui/separator.tsx | 28 + .../src/components/ui/sheet.tsx | 143 + .../src/components/ui/sidebar.tsx | 727 + .../src/components/ui/skeleton.tsx | 13 + .../src/components/ui/sonner.tsx | 38 + .../src/components/ui/spinner.tsx | 16 + .../src/components/ui/switch.tsx | 33 + .../src/components/ui/table.tsx | 115 + .../src/components/ui/tabs.tsx | 91 + .../src/components/ui/toggle-group.tsx | 83 + .../src/components/ui/toggle.tsx | 47 + .../src/components/ui/tooltip.tsx | 55 + .../src/context/AuthContext.tsx | 88 + .../src/hooks/use-mobile.ts | 19 + .../src/hooks/useCountryFromIP.ts | 61 + .../src/hooks/usePageTitle.ts | 12 + .../src/hooks/useTableHeight.ts | 36 + .../apps/artcraft-dashboard/src/index.css | 136 + .../src/layouts/DashboardLayout.tsx | 213 + .../src/lib/feature-flag-icon.ts | 139 + .../artcraft-dashboard/src/lib/identicon.ts | 26 + .../apps/artcraft-dashboard/src/lib/stripe.ts | 12 + .../apps/artcraft-dashboard/src/lib/utils.ts | 20 + frontend/apps/artcraft-dashboard/src/main.tsx | 10 + .../src/pages/DashboardHome.tsx | 169 + .../src/pages/DebugLogs.tsx | 415 + .../src/pages/DebugLogsSearch.tsx | 20 + .../src/pages/ExploreMedia.tsx | 338 + .../src/pages/FeatureFlags.tsx | 285 + .../src/pages/Impersonation.tsx | 315 + .../src/pages/JobHistory.tsx | 194 + .../artcraft-dashboard/src/pages/JobInfo.tsx | 650 + .../src/pages/JobTokenSearch.tsx | 18 + .../artcraft-dashboard/src/pages/Login.tsx | 133 + .../artcraft-dashboard/src/pages/NotFound.tsx | 25 + .../src/pages/ReferralsAnalytics.tsx | 358 + .../src/pages/SendPager.tsx | 250 + .../src/pages/StaffAuditLogs.tsx | 311 + .../src/pages/StripeLookup.tsx | 214 + .../src/pages/SubscriberSignups.tsx | 314 + .../src/pages/UserCreations.tsx | 204 + .../src/pages/UserEmailChanges.tsx | 348 + .../src/pages/UserProfile.tsx | 1220 ++ .../src/pages/UserReferrals.tsx | 359 + .../src/pages/UserSearch.tsx | 396 + .../src/pages/UserSignups.tsx | 301 + .../src/pages/WalletHistory.tsx | 469 + .../artcraft-dashboard/src/types/index.ts | 211 + .../apps/artcraft-dashboard/tsconfig.app.json | 50 + .../apps/artcraft-dashboard/tsconfig.json | 19 + .../artcraft-dashboard/tsconfig.spec.json | 39 + .../apps/artcraft-dashboard/vite.config.ts | 58 + frontend/package-lock.json | 11465 ++++------------ frontend/package.json | 6 +- frontend/tsconfig.json | 3 + 117 files changed, 21558 insertions(+), 8711 deletions(-) create mode 100644 frontend/apps/artcraft-dashboard/.claude/CLAUDE.md create mode 100644 frontend/apps/artcraft-dashboard/.claude/hooks/vexp-guard.sh create mode 100644 frontend/apps/artcraft-dashboard/.claude/settings.json create mode 100644 frontend/apps/artcraft-dashboard/.claude/settings.local.json create mode 100644 frontend/apps/artcraft-dashboard/.vscode/settings.json create mode 100644 frontend/apps/artcraft-dashboard/README.md create mode 100644 frontend/apps/artcraft-dashboard/_redirects create mode 100644 frontend/apps/artcraft-dashboard/components.json create mode 100644 frontend/apps/artcraft-dashboard/eslint.config.js create mode 100644 frontend/apps/artcraft-dashboard/index.html create mode 100644 frontend/apps/artcraft-dashboard/netlify.toml create mode 100644 frontend/apps/artcraft-dashboard/package.json create mode 100644 frontend/apps/artcraft-dashboard/public/_redirects create mode 100644 frontend/apps/artcraft-dashboard/public/artcraft-icon.svg create mode 100644 frontend/apps/artcraft-dashboard/script/netlify_build.sh create mode 100644 frontend/apps/artcraft-dashboard/src/App.css create mode 100644 frontend/apps/artcraft-dashboard/src/App.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/api/ApiManager.ts create mode 100644 frontend/apps/artcraft-dashboard/src/api/MediaApi.ts create mode 100644 frontend/apps/artcraft-dashboard/src/api/ModerationApi.ts create mode 100644 frontend/apps/artcraft-dashboard/src/api/UsersApi.ts create mode 100644 frontend/apps/artcraft-dashboard/src/app/dashboard/data.json create mode 100644 frontend/apps/artcraft-dashboard/src/assets/artcraft-icon.svg create mode 100644 frontend/apps/artcraft-dashboard/src/assets/react.svg create mode 100644 frontend/apps/artcraft-dashboard/src/components/BanUserDialog.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ChangeUserEmailDialog.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/GiveCreditsDialog.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ImpersonateUserDialog.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/JobsTable.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/MediaCard.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/MediaDetailModal.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/RawTag.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/TokenSearchForm.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/UserFeatureFlagsDialog.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/UserLookupPanel.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/app-sidebar.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/chart-area-interactive.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/data-table.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/nav-documents.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/nav-main.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/nav-secondary.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/nav-user.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/section-cards.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/settings-dialog.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/site-header.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/theme-provider.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/alert-dialog.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/alert.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/avatar.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/badge.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/breadcrumb.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/button.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/card.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/chart.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/checkbox.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/dialog.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/drawer.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/dropdown-menu.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/field.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/input.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/label.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/select.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/separator.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/sheet.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/sidebar.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/skeleton.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/sonner.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/spinner.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/switch.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/table.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/tabs.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/toggle-group.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/toggle.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/components/ui/tooltip.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/context/AuthContext.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/hooks/use-mobile.ts create mode 100644 frontend/apps/artcraft-dashboard/src/hooks/useCountryFromIP.ts create mode 100644 frontend/apps/artcraft-dashboard/src/hooks/usePageTitle.ts create mode 100644 frontend/apps/artcraft-dashboard/src/hooks/useTableHeight.ts create mode 100644 frontend/apps/artcraft-dashboard/src/index.css create mode 100644 frontend/apps/artcraft-dashboard/src/layouts/DashboardLayout.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/lib/feature-flag-icon.ts create mode 100644 frontend/apps/artcraft-dashboard/src/lib/identicon.ts create mode 100644 frontend/apps/artcraft-dashboard/src/lib/stripe.ts create mode 100644 frontend/apps/artcraft-dashboard/src/lib/utils.ts create mode 100644 frontend/apps/artcraft-dashboard/src/main.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/DashboardHome.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/DebugLogs.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/DebugLogsSearch.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/ExploreMedia.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/FeatureFlags.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/Impersonation.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/JobHistory.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/JobInfo.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/JobTokenSearch.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/Login.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/NotFound.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/ReferralsAnalytics.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/SendPager.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/StaffAuditLogs.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/StripeLookup.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/SubscriberSignups.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/UserCreations.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/UserEmailChanges.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/UserProfile.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/UserReferrals.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/UserSearch.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/UserSignups.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/pages/WalletHistory.tsx create mode 100644 frontend/apps/artcraft-dashboard/src/types/index.ts create mode 100644 frontend/apps/artcraft-dashboard/tsconfig.app.json create mode 100644 frontend/apps/artcraft-dashboard/tsconfig.json create mode 100644 frontend/apps/artcraft-dashboard/tsconfig.spec.json create mode 100644 frontend/apps/artcraft-dashboard/vite.config.ts diff --git a/frontend/apps/artcraft-dashboard/.claude/CLAUDE.md b/frontend/apps/artcraft-dashboard/.claude/CLAUDE.md new file mode 100644 index 0000000000..59af4f7a0c --- /dev/null +++ b/frontend/apps/artcraft-dashboard/.claude/CLAUDE.md @@ -0,0 +1,60 @@ +## vexp - Context-Aware AI Coding + +### MANDATORY: use vexp pipeline - do NOT grep or glob the codebase +For every task - bug fixes, features, refactors, debugging: +**call `run_pipeline` FIRST**. It executes context search + impact analysis + +memory recall in a single call, returning compressed results. + +Do NOT use grep, glob, Bash, or cat to search/explore the codebase. +vexp returns pre-indexed, graph-ranked context that is more relevant and +uses fewer tokens than manual searching. Prefer `get_skeleton` over Read to +inspect files (detail: minimal/standard/detailed, 70-90% token savings). +Only use Read when you need exact raw content to edit a specific line. + +### Primary Tool +- `run_pipeline` - **USE THIS FOR EVERYTHING**. Single call that runs + capsule + impact + memory server-side. Returns compressed results. + Auto-detects intent (debug/modify/refactor/explore) from your task. + Includes full file content for pivots. + Examples: + - `run_pipeline({ "task": "fix JWT validation bug" })` - auto-detect + - `run_pipeline({ "task": "refactor db layer", "preset": "refactor" })` - explicit + - `run_pipeline({ "task": "add auth", "observation": "using JWT" })` - save insight in same call + +### Other MCP tools (use only when run_pipeline is insufficient) +- `get_skeleton` - **preferred over Read** for inspecting files (minimal/standard/detailed detail levels, 70-90% token savings) +- `index_status` - indexing status and health check +- `expand_vexp_ref` - expand V-REF hash placeholders in v2 compact output + +### Workflow +1. `run_pipeline("your task")` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call +2. Need more detail on a file? Use `get_skeleton({ files: [...], detail: "detailed" })` - avoid Read unless editing +3. Make targeted changes based on the context returned +4. `run_pipeline` again ONLY if you need more context during implementation +5. Do NOT chain multiple vexp calls - one `run_pipeline` replaces capsule + impact + memory + observation + +### Subagent / Explore / Plan mode +- Subagents CAN and MUST call `run_pipeline` - always include the task description +- The PreToolUse hook blocks Grep/Glob when vexp daemon is running +- Do NOT spawn Agent(Explore) to freely search - call `run_pipeline` first, + then pass the returned context into the agent prompt if needed +- Always: `run_pipeline` -> get context -> spawn agent with context + +### Smart Features (automatic - no action needed) +- **Intent Detection**: auto-detects from your task keywords. "fix bug" -> Debug, "refactor" -> blast-radius, "add" -> Modify +- **Hybrid Search**: keyword + semantic + graph centrality ranking +- **Session Memory**: auto-captures observations; memories auto-surfaced in results +- **LSP Bridge**: VS Code captures type-resolved call edges +- **Change Coupling**: co-changed files included as related context + +### Advanced Parameters +- `preset: "debug"` - forces debug mode (capsule+tests+impact+memory) +- `preset: "refactor"` - deep impact analysis (depth 5) +- `max_tokens: 12000` - increase total budget for complex tasks +- `include_tests: true` - include test files in results +- `include_file_content: false` - omit full file content (lighter response) + +### Multi-Repo Workspaces +`run_pipeline` auto-queries all indexed repos. Use `repos: ["alias"]` to scope. +Use `index_status` to discover available repo aliases. + \ No newline at end of file diff --git a/frontend/apps/artcraft-dashboard/.claude/hooks/vexp-guard.sh b/frontend/apps/artcraft-dashboard/.claude/hooks/vexp-guard.sh new file mode 100644 index 0000000000..9cefc1dfcf --- /dev/null +++ b/frontend/apps/artcraft-dashboard/.claude/hooks/vexp-guard.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# vexp-guard: block Grep/Glob when vexp daemon is running AND index is healthy. +# Fast path: if socket file or healthy marker doesn't exist, allow immediately. +# PID check: verify daemon process is alive (handles stale files after kill -9). +VEXP_DIR="${CLAUDE_PROJECT_DIR:-.}/.vexp" +SOCK="$VEXP_DIR/daemon.sock" +HEALTHY="$VEXP_DIR/healthy" +PID_FILE="$VEXP_DIR/daemon.pid" +if [ -S "$SOCK" ] && [ -f "$HEALTHY" ] && [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; then + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"vexp daemon is running. Use run_pipeline instead of Grep/Glob."}}' +else + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"vexp index not ready, allowing direct search fallback."}}' +fi +exit 0 diff --git a/frontend/apps/artcraft-dashboard/.claude/settings.json b/frontend/apps/artcraft-dashboard/.claude/settings.json new file mode 100644 index 0000000000..3883ca63fe --- /dev/null +++ b/frontend/apps/artcraft-dashboard/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Grep|Glob|Regex", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-guard.sh", + "timeout": 3000 + } + ] + } + ] + } +} diff --git a/frontend/apps/artcraft-dashboard/.claude/settings.local.json b/frontend/apps/artcraft-dashboard/.claude/settings.local.json new file mode 100644 index 0000000000..3d7789fb07 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(npx shadcn@latest add:*)", + "Bash(node -e:*)", + "WebFetch(domain:docs.gravatar.com)" + ] + } +} diff --git a/frontend/apps/artcraft-dashboard/.vscode/settings.json b/frontend/apps/artcraft-dashboard/.vscode/settings.json new file mode 100644 index 0000000000..7fcaad65dc --- /dev/null +++ b/frontend/apps/artcraft-dashboard/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.vexp/daemon.pid": true, + "**/.vexp/daemon.sock": true, + "**/.vexp/daemon.pipe": true, + "**/.vexp/daemon.log": true, + "**/.vexp/vexp.log": true, + "**/.vexp/healthy": true, + "**/.vexp/mcp.port": true, + "**/.vexp/mcp.token": true, + "**/.vexp/index.db": true, + "**/.vexp/index.db-wal": true, + "**/.vexp/index.db-shm": true, + "**/.vexp/parent_workspace.json": true, + "**/.vexp/.gitignore": true, + "**/.vexp/.gitattributes": true + } +} \ No newline at end of file diff --git a/frontend/apps/artcraft-dashboard/README.md b/frontend/apps/artcraft-dashboard/README.md new file mode 100644 index 0000000000..9e8a61708e --- /dev/null +++ b/frontend/apps/artcraft-dashboard/README.md @@ -0,0 +1,2 @@ +# ArtCraft Admin Dashboard +Admin dashboard for managing ArtCraft diff --git a/frontend/apps/artcraft-dashboard/_redirects b/frontend/apps/artcraft-dashboard/_redirects new file mode 100644 index 0000000000..b06d6fa10f --- /dev/null +++ b/frontend/apps/artcraft-dashboard/_redirects @@ -0,0 +1,41 @@ +# Netlify redirects for the ArtCraft admin dashboard. +# Rules are evaluated top-to-bottom; the first match wins, so the SPA +# fallback MUST stay last. + +# API proxy — mirrors the dev Vite proxy in vite.config.ts. The app's +# ApiManager defaults its host to "/api" (see src/api/ApiManager.ts), so +# in production "/api/v1/foo" must be forwarded to the real API as +# "/v1/foo". status 200 = proxy (no client-visible redirect). +/api/* https://api.storyteller.ai/:splat 200 + +# SPA fallback — this is a client-side React Router app (BrowserRouter in +# src/App.tsx). Every page below is rendered client-side, so every deep +# link (including dynamic params) must serve index.html and let the router +# take over. A per-page list is unnecessary and would break param routes; +# the single catch-all covers them all: +# +# /login +# / (DashboardHome) +# /user/search +# /user/profile/:username +# /user/profile/:username/wallet +# /user/profile/:username/jobs +# /user/profile/:username/creations +# /user/profile/:username/referrals +# /stripe-lookup +# /user-signups +# /subscriber-signups +# /impersonation +# /staff-audit-logs +# /send-pager +# /feature-flags +# /email-changes +# /referrals +# /explore/media +# /explore/media/:mediaToken +# /moderation/job-search +# /moderation/job/:jobToken +# /moderation/debug-logs-search +# /moderation/debug-logs/:eventToken +# +/* /index.html 200 diff --git a/frontend/apps/artcraft-dashboard/components.json b/frontend/apps/artcraft-dashboard/components.json new file mode 100644 index 0000000000..2a9908f264 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/components.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks", + "context": "@/context" + }, + "registries": {} +} diff --git a/frontend/apps/artcraft-dashboard/eslint.config.js b/frontend/apps/artcraft-dashboard/eslint.config.js new file mode 100644 index 0000000000..5e6b472f58 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/apps/artcraft-dashboard/index.html b/frontend/apps/artcraft-dashboard/index.html new file mode 100644 index 0000000000..1b06962dd0 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + ArtCraft Admin Dashboard + + +
+ + + diff --git a/frontend/apps/artcraft-dashboard/netlify.toml b/frontend/apps/artcraft-dashboard/netlify.toml new file mode 100644 index 0000000000..4fa3bad536 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/netlify.toml @@ -0,0 +1,15 @@ +# Netlify build configuration for the ArtCraft admin dashboard. +# The base directory in the Netlify site settings should be `frontend/` +# (the Nx workspace root), so these paths are relative to that. + +[build] +command = "bash ./apps/artcraft-dashboard/script/netlify_build.sh" +publish = "apps/artcraft-dashboard/dist/" + +# Vite emits content-hashed assets under /assets/*, so they can be cached +# forever — a new deploy produces new filenames. +[[headers]] +for = "/assets/*" + +[headers.values] +"Cache-Control" = "public, max-age=31560000, immutable" diff --git a/frontend/apps/artcraft-dashboard/package.json b/frontend/apps/artcraft-dashboard/package.json new file mode 100644 index 0000000000..a783990955 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/package.json @@ -0,0 +1,42 @@ +{ + "name": "@frontend/artcraft-dashboard", + "version": "0.0.1", + "private": true, + "type": "module", + "dependencies": { + "@dicebear/collection": "^9.4.0", + "@dicebear/core": "^9.4.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@tabler/icons-react": "^3.38.0", + "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-table": "^8.21.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "country-flag-icons": "^1.6.15", + "lucide-react": "^0.577.0", + "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", + "react-router-dom": "^7.13.1", + "recharts": "2.15.4", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.1", + "vaul": "^1.1.2", + "zod": "^4.3.6" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.11.0", + "@vitejs/plugin-react-swc": "^4.2.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "shadcn": "^3.8.5", + "tw-animate-css": "^1.4.0", + "typescript-eslint": "^8.48.0" + } +} diff --git a/frontend/apps/artcraft-dashboard/public/_redirects b/frontend/apps/artcraft-dashboard/public/_redirects new file mode 100644 index 0000000000..5192543dd7 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/public/_redirects @@ -0,0 +1,2 @@ +/api/* https://api.storyteller.ai/:splat 200 +/* /index.html 200 diff --git a/frontend/apps/artcraft-dashboard/public/artcraft-icon.svg b/frontend/apps/artcraft-dashboard/public/artcraft-icon.svg new file mode 100644 index 0000000000..efc0be9e7f --- /dev/null +++ b/frontend/apps/artcraft-dashboard/public/artcraft-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/apps/artcraft-dashboard/script/netlify_build.sh b/frontend/apps/artcraft-dashboard/script/netlify_build.sh new file mode 100644 index 0000000000..683b93042b --- /dev/null +++ b/frontend/apps/artcraft-dashboard/script/netlify_build.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euxo pipefail + +echo "Build the dashboard" +npx nx build artcraft-dashboard + +echo "Change to project dir" +pushd apps/artcraft-dashboard/ + +echo "Copy netlify configs into dist" +cp _redirects dist/ + +echo "List final files in build" +find dist/ + +popd diff --git a/frontend/apps/artcraft-dashboard/src/App.css b/frontend/apps/artcraft-dashboard/src/App.css new file mode 100644 index 0000000000..b9d355df2a --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/apps/artcraft-dashboard/src/App.tsx b/frontend/apps/artcraft-dashboard/src/App.tsx new file mode 100644 index 0000000000..3fcd83ea00 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/App.tsx @@ -0,0 +1,103 @@ +import { BrowserRouter, Routes, Route, Navigate, useLocation, useSearchParams } from "react-router-dom"; +import { AuthProvider, useAuth } from "@/context/AuthContext"; +import { DashboardLayout } from "@/layouts/DashboardLayout"; +import { LoginPage } from "@/pages/Login"; +import { DashboardHome } from "@/pages/DashboardHome"; +import { UserSearch } from "@/pages/UserSearch"; +import { UserProfile } from "@/pages/UserProfile"; +import { WalletHistory } from "@/pages/WalletHistory"; +import { StripeLookup } from "@/pages/StripeLookup"; +import { JobHistory } from "@/pages/JobHistory"; +import { UserCreations } from "@/pages/UserCreations"; +import { UserSignups } from "@/pages/UserSignups"; +import { SubscriberSignups } from "@/pages/SubscriberSignups"; +import { ExploreMedia } from "@/pages/ExploreMedia"; +import { Impersonation } from "@/pages/Impersonation"; +import { StaffAuditLogs } from "@/pages/StaffAuditLogs"; +import { SendPager } from "@/pages/SendPager"; +import { FeatureFlags } from "@/pages/FeatureFlags"; +import { UserEmailChanges } from "@/pages/UserEmailChanges"; +import { ReferralsAnalytics } from "@/pages/ReferralsAnalytics"; +import { UserReferrals } from "@/pages/UserReferrals"; +import { JobInfo } from "@/pages/JobInfo"; +import { JobTokenSearch } from "@/pages/JobTokenSearch"; +import { DebugLogs } from "@/pages/DebugLogs"; +import { DebugLogsSearch } from "@/pages/DebugLogsSearch"; +import { NotFoundPage } from "@/pages/NotFound"; +import { Spinner } from "./components/ui/spinner"; +import { ThemeProvider } from "@/components/theme-provider"; + +function AppRoutes() { + const { user, isLoading } = useAuth(); + const location = useLocation(); + const [searchParams] = useSearchParams(); + const redirectTo = searchParams.get("redirect") || "/"; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( + + : } + /> + + : } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } + /> + + + {/* 404 */} + } /> + + ); +} + +function App() { + return ( + + + + + + + + ); +} + +export default App; diff --git a/frontend/apps/artcraft-dashboard/src/api/ApiManager.ts b/frontend/apps/artcraft-dashboard/src/api/ApiManager.ts new file mode 100644 index 0000000000..4551637ad6 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/api/ApiManager.ts @@ -0,0 +1,124 @@ +export interface ApiResponse { + success: boolean; + errorMessage?: string; + data?: T; + pagination?: P; +} + +const getApiHost = () => { + return import.meta.env.VITE_API_HOST || "/api"; +}; + +export class ApiManager { + protected getApiSchemeAndHost(): string { + return getApiHost(); + } + + public async fetch( + endpoint: string, + { + method, + query, + body, + }: { + method: string; + query?: Record; + body?: B; + }, + ): Promise { + const queryInString = + query && + Object.entries(query).reduce( + (allOptions, [key, value]) => { + if (!value) { + return allOptions; + } + allOptions[key] = value.toString(); + return allOptions; + }, + {} as Record, + ); + + const endpointWithQueries = queryInString + ? endpoint + "?" + new URLSearchParams(queryInString) + : endpoint; + + const bodyInString = body ? JSON.stringify(body) : undefined; + + const response = await fetch(endpointWithQueries, { + method, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + credentials: "include", + body: bodyInString, + }); + + const text = await response.text(); + try { + return JSON.parse(text); + } catch { + console.error("API Error Response:", text); + throw new Error(text || `HTTP ${response.status}`); + } + } + + protected get({ + endpoint, + query, + }: { + endpoint: string; + query?: Record; + }): Promise { + return this.fetch(endpoint, { method: "GET", query }); + } + + protected post({ + endpoint, + query, + body, + }: { + endpoint: string; + query?: Record; + body?: B; + }): Promise { + return this.fetch(endpoint, { + method: "POST", + query, + body, + }); + } + + protected put({ + endpoint, + query, + body, + }: { + endpoint: string; + query?: Record; + body?: B; + }): Promise { + return this.fetch(endpoint, { + method: "PUT", + query, + body, + }); + } + + protected delete({ + endpoint, + query, + body, + }: { + endpoint: string; + query?: Record; + body?: B; + }): Promise { + return this.fetch(endpoint, { + method: "DELETE", + query, + body, + }); + } +} diff --git a/frontend/apps/artcraft-dashboard/src/api/MediaApi.ts b/frontend/apps/artcraft-dashboard/src/api/MediaApi.ts new file mode 100644 index 0000000000..9b3037e48c --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/api/MediaApi.ts @@ -0,0 +1,305 @@ +import { ApiManager, type ApiResponse } from "./ApiManager"; + +export interface Pagination { + total_items: number; + total_pages: number; + current_page: number; + items_per_page: number; +} + +export interface ListUserMediaQuery { + page_index?: number; + page_size?: number; + filter_media_classes?: string[]; + filter_media_type?: string[]; + filter_engine_categories?: string[]; +} + +export interface ListMediaFilesQuery { + sort_ascending?: boolean; + page_size?: number; + cursor?: string; + cursor_is_reversed?: boolean; + filter_media_classes?: string; + filter_media_type?: string; + filter_engine_categories?: string; + include_user_uploads?: boolean; +} + +export interface CursorPagination { + cursor_is_reversed: boolean; + maybe_next: string | null; + maybe_previous: string | null; +} + +export interface ExploreMediaFile { + token: string; + media_class: string; + media_type: string; + created_at: string; + updated_at: string; + maybe_title: string | null; + maybe_text_transcript: string | null; + creator_set_visibility: string; + is_user_upload: boolean; + is_intermediate_system_file: boolean; + media_links: { + cdn_url: string; + maybe_thumbnail_template: string | null; + maybe_video_previews: { + animated?: string; + thumbnail_template?: string; + } | null; + }; + cover_image: { + default_cover: { + color_index: number; + image_index: number; + }; + maybe_cover_image_public_bucket_url: string | null; + maybe_links: { + cdn_url: string; + maybe_thumbnail_template: string | null; + } | null; + }; + maybe_creator: { + default_avatar: { color_index: number; image_index: number }; + display_name: string; + gravatar_hash: string; + user_token: string; + username: string; + } | null; + maybe_duration_millis: number | null; + maybe_engine_category: string | null; + origin: { + origin_category: string; + product_category: string; + }; + stats: { + bookmark_count: number; + positive_rating_count: number; + }; +} + +export interface MediaInfo { + token: string; + media_class: string; + media_type: string; + public_bucket_url?: string; + media_links?: { + cdn_url?: string; + maybe_thumbnail_template?: string; + maybe_video_previews?: { + animated?: string; + thumbnail_template?: string; + } | null; + }; + maybe_title?: string; + maybe_text_transcript?: string; + created_at?: string; + updated_at?: string; + creator_set_visibility?: string; + // include arbitrary keys + [key: string]: any; +} + +export function getMediaThumbnailUrl( + item: MediaInfo | ExploreMediaFile, + width = 512, +): string | undefined { + const widthStr = width.toString(); + + // Videos: use animated preview + if (item.media_class === "video") { + if (item.media_links?.maybe_video_previews?.animated) { + return item.media_links.maybe_video_previews.animated; + } + } + + // 3D/dimensional: use cover image URL + if (item.media_class === "dimensional") { + if ("cover_image" in item && item.cover_image) { + if (item.cover_image.maybe_cover_image_public_bucket_url) { + return item.cover_image.maybe_cover_image_public_bucket_url; + } + if (item.cover_image.maybe_links?.maybe_thumbnail_template) { + return item.cover_image.maybe_links.maybe_thumbnail_template.replace( + "{WIDTH}", + widthStr, + ); + } + if (item.cover_image.maybe_links?.cdn_url) { + return item.cover_image.maybe_links.cdn_url; + } + } + } + + // Images (and fallback): use thumbnail template + if (item.media_links?.maybe_thumbnail_template) { + return item.media_links.maybe_thumbnail_template.replace( + "{WIDTH}", + widthStr, + ); + } + + if (item.media_links?.cdn_url) return item.media_links.cdn_url; + if ("public_bucket_url" in item && item.public_bucket_url) + return item.public_bucket_url as string; + return undefined; +} + +export interface MediaFileDetail { + token: string; + maybe_prompt_token: string | null; + media_class: string; + media_type: string; + maybe_title: string | null; + maybe_text_transcript: string | null; + [key: string]: any; +} + +export interface PromptInfo { + token: string; + maybe_model_type: string | null; + maybe_model_class: string | null; + maybe_positive_prompt: string | null; + maybe_negative_prompt: string | null; + maybe_aspect_ratio: string | null; + maybe_resolution: string | null; + maybe_generation_provider: string | null; + maybe_generation_mode: string | null; + maybe_duration_seconds: number | null; + maybe_inference_duration_millis: number | null; +} + +export class MediaApi extends ApiManager { + public async ListMediaFiles( + query: ListMediaFilesQuery = {}, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/media_files/list`; + + const queryParams: Record = {}; + + if (query.page_size !== undefined) queryParams.page_size = query.page_size; + if (query.cursor) queryParams.cursor = query.cursor; + if (query.sort_ascending !== undefined) + queryParams.sort_ascending = query.sort_ascending; + if (query.cursor_is_reversed !== undefined) + queryParams.cursor_is_reversed = query.cursor_is_reversed; + if (query.filter_media_classes) + queryParams.filter_media_classes = query.filter_media_classes; + if (query.filter_media_type) + queryParams.filter_media_type = query.filter_media_type; + if (query.filter_engine_categories) + queryParams.filter_engine_categories = query.filter_engine_categories; + if (query.include_user_uploads) + queryParams.include_user_uploads = query.include_user_uploads; + + return await this.get<{ + success: boolean; + results: ExploreMediaFile[]; + pagination: CursorPagination; + error_message?: string; + }>({ endpoint, query: queryParams }) + .then((response) => ({ + success: response.success, + data: response.results ?? [], + pagination: response.pagination, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListUserMediaFiles( + username: string, + query: ListUserMediaQuery = {}, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/media_files/list/user/${username}`; + + // Convert to fully stringified dict + const queryWithStrings: Record = { + include_user_uploads: true, + }; + + if (query.page_index !== undefined) + queryWithStrings.page_index = query.page_index; + if (query.page_size !== undefined) + queryWithStrings.page_size = query.page_size; + + if (query.filter_media_classes) { + queryWithStrings.filter_media_classes = + query.filter_media_classes.join(","); + } + if (query.filter_media_type) { + queryWithStrings.filter_media_type = query.filter_media_type.join(","); + } + if (query.filter_engine_categories) { + queryWithStrings.filter_engine_categories = + query.filter_engine_categories.join(","); + } + + return await this.get<{ + success: boolean; + results: MediaInfo[]; + pagination?: Pagination; + error_message?: string; + }>({ endpoint, query: queryWithStrings }) + .then((response) => ({ + success: response.success, + data: response.results ?? [], + pagination: response.pagination, + errorMessage: response.error_message, + })) + .catch((err) => { + return { + success: false, + errorMessage: err.message, + }; + }); + } + + public async GetMediaFile( + token: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/media_files/file/${token}`; + + return await this.get<{ + success: boolean; + media_file: MediaFileDetail; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: response.media_file, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async GetPrompt( + token: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/prompts/${token}`; + + return await this.get<{ + success: boolean; + prompt: PromptInfo; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: response.prompt, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } +} diff --git a/frontend/apps/artcraft-dashboard/src/api/ModerationApi.ts b/frontend/apps/artcraft-dashboard/src/api/ModerationApi.ts new file mode 100644 index 0000000000..23e94f082d --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/api/ModerationApi.ts @@ -0,0 +1,798 @@ +import { ApiManager, type ApiResponse } from "./ApiManager"; +import type { + Wallet, + WalletWithOwner, + WalletLedgerEntry, + WalletLedgerEntryWithWallet, + UserJob, + SignupUser, + SubscriberUser, + ImpersonationRequest, + StaffAuditLog, + ModerationJob, + DebugLog, + UserEmailChange, +} from "@/types"; + +export interface FeatureFlagDescriptor { + key: string; + full_name: string; + description: string; +} + +export interface UserFeatureFlagsResponse { + user_token: string; + username: string; + display_name: string; + feature_flags: string[]; +} + +export type EditUserFeatureFlagsAction = + | { AddFlags: { flags: string[] } } + | { RemoveFlags: { flags: string[] } } + | { KeepFlags: { flags: string[] } } + | { SetExactFlags: { flags: string[] } } + | "ClearAllFlags"; + +export interface UserReferralInvitedUser { + token: string; + username: string; + display_name: string; + email_address: string; +} + +export interface UserReferralReferrerUser { + token: string; + username: string; + display_name: string; +} + +export interface UserReferralListItem { + created_at: string; + invited_user: UserReferralInvitedUser; + referrer_user: UserReferralReferrerUser; + maybe_landing_url: string | null; + maybe_referral_url: string | null; + maybe_referral_code_token: string | null; +} + +/** Which table the customer id was found in. */ +export type ModeratorStripeCustomerIdSource = "customer_link" | "subscription"; + +export interface ModeratorUserStripeCustomerIdEntry { + stripe_customer_id: string; + payments_namespace: string; + source: ModeratorStripeCustomerIdSource; +} + +export interface ModeratorUserLookupResponse { + display_name: string; + email_address: string; + email_confirmed: boolean; + email_gravatar_hash: string; + email_is_synthetic: boolean; + ip_address_creation: string; + ip_address_last_login: string; + is_banned: boolean; + is_temporary: boolean; + is_without_password: boolean; + maybe_avatar_media_file_token?: string; + created_at?: string; + token: string; + username: string; + username_is_generated: boolean; + username_is_not_customized: boolean; +} + +export class ModerationApi extends ApiManager { + public async UserLookup( + search: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/users/lookup`; + return await this.post< + { search: string }, + { + success: boolean; + maybe_user?: ModeratorUserLookupResponse; + error_message?: string; + } + >({ + endpoint, + body: { search }, + }) + .then((response) => ({ + success: response.success, + data: { + maybe_user: response.maybe_user, + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async LookupByStripeCustomerId(stripe_customer_id: string): Promise< + ApiResponse<{ + users: Array<{ + display_name: string; + email_address: string; + maybe_stripe_subscription_id?: string | null; + subscription_namespace: string; + token: string; + username: string; + }>; + }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/users/lookup_by_stripe_customer_id`; + return await this.post< + { stripe_customer_id: string }, + { + success: boolean; + users: Array<{ + display_name: string; + email_address: string; + maybe_stripe_subscription_id?: string | null; + subscription_namespace: string; + token: string; + username: string; + }>; + error_message?: string; + } + >({ + endpoint, + body: { stripe_customer_id }, + }) + .then((response) => ({ + success: response.success, + data: { + users: response.users || [], + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async GetUserStripeCustomerIds( + user_token: string, + ): Promise< + ApiResponse<{ customer_ids: ModeratorUserStripeCustomerIdEntry[] }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/user_stripe_data/${encodeURIComponent(user_token)}/customer_ids`; + return await this.get<{ + success: boolean; + customer_ids: ModeratorUserStripeCustomerIdEntry[]; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: { customer_ids: response.customer_ids || [] }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ImpersonateUser(params: { + username?: string; + user_token?: string; + email_address?: string; + username_email_or_token?: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/user_sessions/impersonate`; + return await this.post< + typeof params, + { + success: boolean; + password_token?: string; + error_message?: string; + } + >({ + endpoint, + body: params, + }) + .then((response) => ({ + success: response.success, + data: response.password_token + ? { password_token: response.password_token } + : undefined, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async SendAlert(params: { + title?: string | null; + description?: string | null; + urgency?: "high" | "medium" | "low" | null; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/alerts/send`; + return await this.post< + typeof params, + { success: boolean; error_message?: string } + >({ endpoint, body: params }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ManageUserBan(params: { + username: string; + is_banned: boolean; + mod_notes: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/moderation/user_bans/manage_ban`; + return await this.post< + typeof params, + { success: boolean; error_message?: string } + >({ endpoint, body: params }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListStaffAuditLogs( + cursor?: string | null, + limit?: number, + ): Promise< + ApiResponse<{ + audit_logs: StaffAuditLog[]; + next_cursor: string | null; + }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/staff_audit_logs/list`; + return await this.get<{ + success: boolean; + audit_logs: StaffAuditLog[]; + maybe_cursor: string | null; + error_message?: string; + }>({ + endpoint, + query: { + cursor: cursor ?? undefined, + limit, + }, + }) + .then((response) => ({ + success: response.success, + data: { + audit_logs: response.audit_logs || [], + next_cursor: response.maybe_cursor ?? null, + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListAllImpersonationRequests( + cursor?: string | null, + limit?: number, + ): Promise< + ApiResponse<{ + impersonation_requests: ImpersonationRequest[]; + next_cursor: string | null; + }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/user_sessions/impersonation_requests/list`; + return await this.get<{ + success: boolean; + impersonation_requests: ImpersonationRequest[]; + maybe_cursor: string | null; + error_message?: string; + }>({ + endpoint, + query: { + cursor: cursor ?? undefined, + limit, + }, + }) + .then((response) => ({ + success: response.success, + data: { + impersonation_requests: response.impersonation_requests || [], + next_cursor: response.maybe_cursor ?? null, + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListUserImpersonationRequests( + user_token: string, + cursor?: string | null, + limit?: number, + ): Promise< + ApiResponse<{ + impersonation_requests: ImpersonationRequest[]; + next_cursor: string | null; + }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/user_sessions/impersonation_requests/user/${user_token}`; + return await this.get<{ + success: boolean; + impersonation_requests: ImpersonationRequest[]; + maybe_cursor: string | null; + error_message?: string; + }>({ + endpoint, + query: { + cursor: cursor ?? undefined, + limit, + }, + }) + .then((response) => ({ + success: response.success, + data: { + impersonation_requests: response.impersonation_requests || [], + next_cursor: response.maybe_cursor ?? null, + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListUserWallets( + user_token: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/wallets/user/${user_token}/list`; + return await this.get<{ + success: boolean; + wallets: Wallet[]; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: { wallets: response.wallets || [] }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async GetWallet( + wallet_token: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/wallet/${wallet_token}`; + return await this.get<{ + success: boolean; + maybe_wallet: WalletWithOwner | null; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: { maybe_wallet: response.maybe_wallet }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async AddBankedBalance( + wallet_token: string, + credits: number, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/wallet/${wallet_token}/add_banked_balance`; + return await this.post< + { credits: number }, + { success: boolean; error_message?: string } + >({ endpoint, body: { credits } }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListWalletLedgerEntries( + wallet_token: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/wallet_ledger_entries/wallet/${wallet_token}/list`; + return await this.get<{ + success: boolean; + entries: WalletLedgerEntryWithWallet[]; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: { entries: response.entries || [] }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async GetWalletLedgerEntry( + wallet_ledger_entry_token: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/wallet_ledger_entry/${wallet_ledger_entry_token}`; + return await this.get<{ + success: boolean; + maybe_entry: WalletLedgerEntry | null; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: { maybe_entry: response.maybe_entry }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async CreateWalletForUser( + user_token: string, + payments_namespace?: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/wallet/create_for_user`; + return await this.post< + { user_token: string; payments_namespace?: string }, + { success: boolean; error_message?: string } + >({ + endpoint, + body: { user_token, payments_namespace }, + }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async GetJobByToken( + job_token: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/job/${encodeURIComponent(job_token)}`; + return await this.get<{ + success: boolean; + job?: ModerationJob | null; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: { maybe_job: response.job ?? null }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListDebugLogs( + event_token: string, + limit?: number, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/debug_logs/list/${encodeURIComponent(event_token)}`; + return await this.get<{ + success: boolean; + debug_logs: DebugLog[]; + error_message?: string; + }>({ + endpoint, + query: { limit }, + }) + .then((response) => ({ + success: response.success, + data: { debug_logs: response.debug_logs || [] }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListUserJobs( + user_token: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/jobs/user/${user_token}/list`; + return await this.get<{ + success: boolean; + jobs: UserJob[]; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: { jobs: response.jobs || [] }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListAllUsersBySignupDate( + id_cursor?: number | null, + ): Promise< + ApiResponse<{ users: SignupUser[]; next_cursor: number | null }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/users/list_all_by_signup_date`; + return await this.post< + { id_cursor?: number | null }, + { + success: boolean; + users: SignupUser[]; + next_cursor: number | null; + error_message?: string; + } + >({ + endpoint, + body: { id_cursor: id_cursor ?? null }, + }) + .then((response) => ({ + success: response.success, + data: { + users: response.users || [], + next_cursor: response.next_cursor ?? null, + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListAvailableFeatureFlags(): Promise< + ApiResponse<{ feature_flags: FeatureFlagDescriptor[] }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/user_feature_flags/list`; + return await this.get<{ + success: boolean; + feature_flags: FeatureFlagDescriptor[]; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: { feature_flags: response.feature_flags || [] }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListUserFeatureFlags( + username_or_token: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/user_feature_flags/user/${encodeURIComponent(username_or_token)}`; + return await this.get< + { success: boolean; error_message?: string } & UserFeatureFlagsResponse + >({ endpoint }) + .then((response) => ({ + success: response.success, + data: { + user_token: response.user_token, + username: response.username, + display_name: response.display_name, + feature_flags: response.feature_flags || [], + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async EditUserFeatureFlags( + username_or_token: string, + action: EditUserFeatureFlagsAction, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/user_feature_flags/user/${encodeURIComponent(username_or_token)}`; + return await this.post< + { action: EditUserFeatureFlagsAction }, + { success: boolean; error_message?: string } + >({ endpoint, body: { action } }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListGlobalUserReferrals( + cursor?: string | null, + limit?: number, + ): Promise< + ApiResponse<{ + referrals: UserReferralListItem[]; + next_cursor: string | null; + }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/user_referrals/list`; + return await this.get<{ + success: boolean; + referrals: UserReferralListItem[]; + maybe_cursor: string | null; + error_message?: string; + }>({ + endpoint, + query: { + cursor: cursor ?? undefined, + limit, + }, + }) + .then((response) => ({ + success: response.success, + data: { + referrals: response.referrals || [], + next_cursor: response.maybe_cursor ?? null, + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListUserReferralsForUser( + username: string, + cursor?: string | null, + limit?: number, + ): Promise< + ApiResponse<{ + referrals: UserReferralListItem[]; + next_cursor: string | null; + }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/user_referrals/user/${encodeURIComponent(username)}/list`; + return await this.get<{ + success: boolean; + referrals: UserReferralListItem[]; + maybe_cursor: string | null; + error_message?: string; + }>({ + endpoint, + query: { + cursor: cursor ?? undefined, + limit, + }, + }) + .then((response) => ({ + success: response.success, + data: { + referrals: response.referrals || [], + next_cursor: response.maybe_cursor ?? null, + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListSubscribersBySignupDate( + id_cursor?: number | null, + ): Promise< + ApiResponse<{ users: SubscriberUser[]; next_cursor: number | null }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/users/list_subscribers_by_signup_date`; + return await this.post< + { id_cursor?: number | null }, + { + success: boolean; + users: SubscriberUser[]; + next_cursor: number | null; + error_message?: string; + } + >({ + endpoint, + body: { id_cursor: id_cursor ?? null }, + }) + .then((response) => ({ + success: response.success, + data: { + users: response.users || [], + next_cursor: response.next_cursor ?? null, + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ChangeUserEmail( + user_token: string, + new_email: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/moderation/user_emails/change`; + return await this.post< + { user_token: string; new_email: string }, + { success: boolean; error_message?: string } + >({ endpoint, body: { user_token, new_email } }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ListUserEmailChanges( + username: string, + cursor?: string | null, + limit?: number, + ): Promise< + ApiResponse<{ changes: UserEmailChange[]; next_cursor: string | null }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/moderation/user_emails/list/${encodeURIComponent(username)}`; + return await this.get<{ + success: boolean; + changes: UserEmailChange[]; + maybe_cursor: string | null; + error_message?: string; + }>({ + endpoint, + query: { + cursor: cursor ?? undefined, + limit, + }, + }) + .then((response) => ({ + success: response.success, + data: { + changes: response.changes || [], + next_cursor: response.maybe_cursor ?? null, + }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } +} diff --git a/frontend/apps/artcraft-dashboard/src/api/UsersApi.ts b/frontend/apps/artcraft-dashboard/src/api/UsersApi.ts new file mode 100644 index 0000000000..62b4fe65af --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/api/UsersApi.ts @@ -0,0 +1,299 @@ +import { ApiManager, type ApiResponse } from "./ApiManager"; +import type { UserInfo, AppStateResponse } from "@/types"; + +interface SignupRequest { + username: string; + email_address: string; + password: string; + password_confirmation: string; + signup_source?: string; +} + +export interface OnboardingData { + email_not_set: boolean; + email_not_confirmed: boolean; + password_not_set: boolean; + username_not_customized: boolean; +} + +export class UsersApi extends ApiManager { + public GetAppState(): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/app_state`; + return this.get({ endpoint }) + .then((response) => ({ + success: response.success, + data: response, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public GetUserProfile(username: string): Promise< + ApiResponse<{ + user?: UserInfo; + }> + > { + const endpoint = `${this.getApiSchemeAndHost()}/v1/user/${username}/profile`; + return this.get<{ + success: boolean; + user?: { + user_token: string; + username: string; + display_name: string; + email_gravatar_hash: string; + disable_gravatar: boolean; + created_at: string; + [key: string]: any; + }; + error_message?: string; + }>({ endpoint }) + .then((response) => ({ + success: response.success, + data: { + user: response.user + ? { + id: response.user.user_token, + username: response.user.username, + display_name: response.user.display_name, + email: "", + gravatar_hash: response.user.email_gravatar_hash, + disable_gravatar: response.user.disable_gravatar, + created_at: response.user.created_at, + } + : undefined, + }, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async Login({ + usernameOrEmail, + password, + }: { + usernameOrEmail: string; + password: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/login`; + const body = { + username_or_email: usernameOrEmail, + password, + }; + return await this.post< + { username_or_email: string; password: string }, + { + success: boolean; + signed_session?: string; + error_message?: string; + error_type?: string; + } + >({ + endpoint, + body, + }) + .then((response) => ({ + success: response.success, + data: { signedSession: response.signed_session }, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public Logout(): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/logout`; + return this.post({ + endpoint, + }) + .then((response) => ({ + success: response.success, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async Signup({ + username, + email, + password, + passwordConfirmation, + signupSource, + }: { + username: string; + email: string; + password: string; + passwordConfirmation: string; + signupSource?: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/create_account`; + const body: SignupRequest = { + email_address: email, + password, + password_confirmation: passwordConfirmation, + username, + }; + if (signupSource) { + body.signup_source = signupSource; + } + return await this.post< + SignupRequest, + { + success: boolean; + signed_session?: string; + error_fields?: Record; + error_message?: string; + error_type?: string; + } + >({ + endpoint, + body, + }) + .then((response) => ({ + success: response.success, + data: { signedSession: response.signed_session }, + errorMessage: + response.error_message || + Object.values(response.error_fields ?? {}).join(", "), + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async RequestPasswordReset({ + email, + }: { + email: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/request_password_reset`; + return await this.post< + { email: string }, + { success: boolean; error_message?: string } + >({ + endpoint, + body: { email }, + }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ResetPassword({ + token, + password, + passwordConfirmation, + }: { + token: string; + password: string; + passwordConfirmation: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/reset_password`; + return await this.post< + { token: string; password: string; password_confirmation: string }, + { success: boolean; error_message?: string } + >({ + endpoint, + body: { + token, + password, + password_confirmation: passwordConfirmation, + }, + }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async ChangePassword({ + password, + passwordConfirmation, + }: { + password: string; + passwordConfirmation: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/user/change_password`; + return await this.post< + { password: string; password_confirmation: string }, + { success: boolean; error_message?: string } + >({ + endpoint, + body: { + password, + password_confirmation: passwordConfirmation, + }, + }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async EditEmail({ + emailAddress, + }: { + emailAddress: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/user/edit_email`; + return await this.post< + { email_address: string }, + { success: boolean; error_message?: string } + >({ + endpoint, + body: { email_address: emailAddress }, + }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public async EditUsername({ + displayName, + }: { + displayName: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/user/edit_username`; + return await this.post< + { display_name: string }, + { success: boolean; error_message?: string } + >({ + endpoint, + body: { display_name: displayName }, + }) + .then((response) => ({ + success: response.success, + errorMessage: response.error_message, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } +} diff --git a/frontend/apps/artcraft-dashboard/src/app/dashboard/data.json b/frontend/apps/artcraft-dashboard/src/app/dashboard/data.json new file mode 100644 index 0000000000..ec0873641b --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/app/dashboard/data.json @@ -0,0 +1,614 @@ +[ + { + "id": 1, + "header": "Cover page", + "type": "Cover page", + "status": "In Process", + "target": "18", + "limit": "5", + "reviewer": "Eddie Lake" + }, + { + "id": 2, + "header": "Table of contents", + "type": "Table of contents", + "status": "Done", + "target": "29", + "limit": "24", + "reviewer": "Eddie Lake" + }, + { + "id": 3, + "header": "Executive summary", + "type": "Narrative", + "status": "Done", + "target": "10", + "limit": "13", + "reviewer": "Eddie Lake" + }, + { + "id": 4, + "header": "Technical approach", + "type": "Narrative", + "status": "Done", + "target": "27", + "limit": "23", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 5, + "header": "Design", + "type": "Narrative", + "status": "In Process", + "target": "2", + "limit": "16", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 6, + "header": "Capabilities", + "type": "Narrative", + "status": "In Process", + "target": "20", + "limit": "8", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 7, + "header": "Integration with existing systems", + "type": "Narrative", + "status": "In Process", + "target": "19", + "limit": "21", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 8, + "header": "Innovation and Advantages", + "type": "Narrative", + "status": "Done", + "target": "25", + "limit": "26", + "reviewer": "Assign reviewer" + }, + { + "id": 9, + "header": "Overview of EMR's Innovative Solutions", + "type": "Technical content", + "status": "Done", + "target": "7", + "limit": "23", + "reviewer": "Assign reviewer" + }, + { + "id": 10, + "header": "Advanced Algorithms and Machine Learning", + "type": "Narrative", + "status": "Done", + "target": "30", + "limit": "28", + "reviewer": "Assign reviewer" + }, + { + "id": 11, + "header": "Adaptive Communication Protocols", + "type": "Narrative", + "status": "Done", + "target": "9", + "limit": "31", + "reviewer": "Assign reviewer" + }, + { + "id": 12, + "header": "Advantages Over Current Technologies", + "type": "Narrative", + "status": "Done", + "target": "12", + "limit": "0", + "reviewer": "Assign reviewer" + }, + { + "id": 13, + "header": "Past Performance", + "type": "Narrative", + "status": "Done", + "target": "22", + "limit": "33", + "reviewer": "Assign reviewer" + }, + { + "id": 14, + "header": "Customer Feedback and Satisfaction Levels", + "type": "Narrative", + "status": "Done", + "target": "15", + "limit": "34", + "reviewer": "Assign reviewer" + }, + { + "id": 15, + "header": "Implementation Challenges and Solutions", + "type": "Narrative", + "status": "Done", + "target": "3", + "limit": "35", + "reviewer": "Assign reviewer" + }, + { + "id": 16, + "header": "Security Measures and Data Protection Policies", + "type": "Narrative", + "status": "In Process", + "target": "6", + "limit": "36", + "reviewer": "Assign reviewer" + }, + { + "id": 17, + "header": "Scalability and Future Proofing", + "type": "Narrative", + "status": "Done", + "target": "4", + "limit": "37", + "reviewer": "Assign reviewer" + }, + { + "id": 18, + "header": "Cost-Benefit Analysis", + "type": "Plain language", + "status": "Done", + "target": "14", + "limit": "38", + "reviewer": "Assign reviewer" + }, + { + "id": 19, + "header": "User Training and Onboarding Experience", + "type": "Narrative", + "status": "Done", + "target": "17", + "limit": "39", + "reviewer": "Assign reviewer" + }, + { + "id": 20, + "header": "Future Development Roadmap", + "type": "Narrative", + "status": "Done", + "target": "11", + "limit": "40", + "reviewer": "Assign reviewer" + }, + { + "id": 21, + "header": "System Architecture Overview", + "type": "Technical content", + "status": "In Process", + "target": "24", + "limit": "18", + "reviewer": "Maya Johnson" + }, + { + "id": 22, + "header": "Risk Management Plan", + "type": "Narrative", + "status": "Done", + "target": "15", + "limit": "22", + "reviewer": "Carlos Rodriguez" + }, + { + "id": 23, + "header": "Compliance Documentation", + "type": "Legal", + "status": "In Process", + "target": "31", + "limit": "27", + "reviewer": "Sarah Chen" + }, + { + "id": 24, + "header": "API Documentation", + "type": "Technical content", + "status": "Done", + "target": "8", + "limit": "12", + "reviewer": "Raj Patel" + }, + { + "id": 25, + "header": "User Interface Mockups", + "type": "Visual", + "status": "In Process", + "target": "19", + "limit": "25", + "reviewer": "Leila Ahmadi" + }, + { + "id": 26, + "header": "Database Schema", + "type": "Technical content", + "status": "Done", + "target": "22", + "limit": "20", + "reviewer": "Thomas Wilson" + }, + { + "id": 27, + "header": "Testing Methodology", + "type": "Technical content", + "status": "In Process", + "target": "17", + "limit": "14", + "reviewer": "Assign reviewer" + }, + { + "id": 28, + "header": "Deployment Strategy", + "type": "Narrative", + "status": "Done", + "target": "26", + "limit": "30", + "reviewer": "Eddie Lake" + }, + { + "id": 29, + "header": "Budget Breakdown", + "type": "Financial", + "status": "In Process", + "target": "13", + "limit": "16", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 30, + "header": "Market Analysis", + "type": "Research", + "status": "Done", + "target": "29", + "limit": "32", + "reviewer": "Sophia Martinez" + }, + { + "id": 31, + "header": "Competitor Comparison", + "type": "Research", + "status": "In Process", + "target": "21", + "limit": "19", + "reviewer": "Assign reviewer" + }, + { + "id": 32, + "header": "Maintenance Plan", + "type": "Technical content", + "status": "Done", + "target": "16", + "limit": "23", + "reviewer": "Alex Thompson" + }, + { + "id": 33, + "header": "User Personas", + "type": "Research", + "status": "In Process", + "target": "27", + "limit": "24", + "reviewer": "Nina Patel" + }, + { + "id": 34, + "header": "Accessibility Compliance", + "type": "Legal", + "status": "Done", + "target": "18", + "limit": "21", + "reviewer": "Assign reviewer" + }, + { + "id": 35, + "header": "Performance Metrics", + "type": "Technical content", + "status": "In Process", + "target": "23", + "limit": "26", + "reviewer": "David Kim" + }, + { + "id": 36, + "header": "Disaster Recovery Plan", + "type": "Technical content", + "status": "Done", + "target": "14", + "limit": "17", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 37, + "header": "Third-party Integrations", + "type": "Technical content", + "status": "In Process", + "target": "25", + "limit": "28", + "reviewer": "Eddie Lake" + }, + { + "id": 38, + "header": "User Feedback Summary", + "type": "Research", + "status": "Done", + "target": "20", + "limit": "15", + "reviewer": "Assign reviewer" + }, + { + "id": 39, + "header": "Localization Strategy", + "type": "Narrative", + "status": "In Process", + "target": "12", + "limit": "19", + "reviewer": "Maria Garcia" + }, + { + "id": 40, + "header": "Mobile Compatibility", + "type": "Technical content", + "status": "Done", + "target": "28", + "limit": "31", + "reviewer": "James Wilson" + }, + { + "id": 41, + "header": "Data Migration Plan", + "type": "Technical content", + "status": "In Process", + "target": "19", + "limit": "22", + "reviewer": "Assign reviewer" + }, + { + "id": 42, + "header": "Quality Assurance Protocols", + "type": "Technical content", + "status": "Done", + "target": "30", + "limit": "33", + "reviewer": "Priya Singh" + }, + { + "id": 43, + "header": "Stakeholder Analysis", + "type": "Research", + "status": "In Process", + "target": "11", + "limit": "14", + "reviewer": "Eddie Lake" + }, + { + "id": 44, + "header": "Environmental Impact Assessment", + "type": "Research", + "status": "Done", + "target": "24", + "limit": "27", + "reviewer": "Assign reviewer" + }, + { + "id": 45, + "header": "Intellectual Property Rights", + "type": "Legal", + "status": "In Process", + "target": "17", + "limit": "20", + "reviewer": "Sarah Johnson" + }, + { + "id": 46, + "header": "Customer Support Framework", + "type": "Narrative", + "status": "Done", + "target": "22", + "limit": "25", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 47, + "header": "Version Control Strategy", + "type": "Technical content", + "status": "In Process", + "target": "15", + "limit": "18", + "reviewer": "Assign reviewer" + }, + { + "id": 48, + "header": "Continuous Integration Pipeline", + "type": "Technical content", + "status": "Done", + "target": "26", + "limit": "29", + "reviewer": "Michael Chen" + }, + { + "id": 49, + "header": "Regulatory Compliance", + "type": "Legal", + "status": "In Process", + "target": "13", + "limit": "16", + "reviewer": "Assign reviewer" + }, + { + "id": 50, + "header": "User Authentication System", + "type": "Technical content", + "status": "Done", + "target": "28", + "limit": "31", + "reviewer": "Eddie Lake" + }, + { + "id": 51, + "header": "Data Analytics Framework", + "type": "Technical content", + "status": "In Process", + "target": "21", + "limit": "24", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 52, + "header": "Cloud Infrastructure", + "type": "Technical content", + "status": "Done", + "target": "16", + "limit": "19", + "reviewer": "Assign reviewer" + }, + { + "id": 53, + "header": "Network Security Measures", + "type": "Technical content", + "status": "In Process", + "target": "29", + "limit": "32", + "reviewer": "Lisa Wong" + }, + { + "id": 54, + "header": "Project Timeline", + "type": "Planning", + "status": "Done", + "target": "14", + "limit": "17", + "reviewer": "Eddie Lake" + }, + { + "id": 55, + "header": "Resource Allocation", + "type": "Planning", + "status": "In Process", + "target": "27", + "limit": "30", + "reviewer": "Assign reviewer" + }, + { + "id": 56, + "header": "Team Structure and Roles", + "type": "Planning", + "status": "Done", + "target": "20", + "limit": "23", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 57, + "header": "Communication Protocols", + "type": "Planning", + "status": "In Process", + "target": "15", + "limit": "18", + "reviewer": "Assign reviewer" + }, + { + "id": 58, + "header": "Success Metrics", + "type": "Planning", + "status": "Done", + "target": "30", + "limit": "33", + "reviewer": "Eddie Lake" + }, + { + "id": 59, + "header": "Internationalization Support", + "type": "Technical content", + "status": "In Process", + "target": "23", + "limit": "26", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 60, + "header": "Backup and Recovery Procedures", + "type": "Technical content", + "status": "Done", + "target": "18", + "limit": "21", + "reviewer": "Assign reviewer" + }, + { + "id": 61, + "header": "Monitoring and Alerting System", + "type": "Technical content", + "status": "In Process", + "target": "25", + "limit": "28", + "reviewer": "Daniel Park" + }, + { + "id": 62, + "header": "Code Review Guidelines", + "type": "Technical content", + "status": "Done", + "target": "12", + "limit": "15", + "reviewer": "Eddie Lake" + }, + { + "id": 63, + "header": "Documentation Standards", + "type": "Technical content", + "status": "In Process", + "target": "27", + "limit": "30", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 64, + "header": "Release Management Process", + "type": "Planning", + "status": "Done", + "target": "22", + "limit": "25", + "reviewer": "Assign reviewer" + }, + { + "id": 65, + "header": "Feature Prioritization Matrix", + "type": "Planning", + "status": "In Process", + "target": "19", + "limit": "22", + "reviewer": "Emma Davis" + }, + { + "id": 66, + "header": "Technical Debt Assessment", + "type": "Technical content", + "status": "Done", + "target": "24", + "limit": "27", + "reviewer": "Eddie Lake" + }, + { + "id": 67, + "header": "Capacity Planning", + "type": "Planning", + "status": "In Process", + "target": "21", + "limit": "24", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 68, + "header": "Service Level Agreements", + "type": "Legal", + "status": "Done", + "target": "26", + "limit": "29", + "reviewer": "Assign reviewer" + } +] diff --git a/frontend/apps/artcraft-dashboard/src/assets/artcraft-icon.svg b/frontend/apps/artcraft-dashboard/src/assets/artcraft-icon.svg new file mode 100644 index 0000000000..efc0be9e7f --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/assets/artcraft-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/apps/artcraft-dashboard/src/assets/react.svg b/frontend/apps/artcraft-dashboard/src/assets/react.svg new file mode 100644 index 0000000000..6c87de9bb3 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/apps/artcraft-dashboard/src/components/BanUserDialog.tsx b/frontend/apps/artcraft-dashboard/src/components/BanUserDialog.tsx new file mode 100644 index 0000000000..4d6a03c835 --- /dev/null +++ b/frontend/apps/artcraft-dashboard/src/components/BanUserDialog.tsx @@ -0,0 +1,216 @@ +import { useEffect, useState } from "react"; +import { ModerationApi } from "@/api/ModerationApi"; +import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Label } from "@/components/ui/label"; +import { + IconAlertCircle, + IconBan, + IconShieldCheck, +} from "@tabler/icons-react"; +import { cn } from "@/lib/utils"; + +interface BanUserDialogProps { + username: string; + displayName: string; + isBanned: boolean; + open: boolean; + onOpenChange: (open: boolean) => void; + onSuccess?: () => void; +} + +export function BanUserDialog({ + username, + displayName, + isBanned, + open, + onOpenChange, + onSuccess, +}: BanUserDialogProps) { + const [modNotes, setModNotes] = useState(""); + const [confirmOpen, setConfirmOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!open) { + setModNotes(""); + setConfirmOpen(false); + setIsSubmitting(false); + setError(null); + } + }, [open]); + + const trimmedNotes = modNotes.trim(); + const isValid = trimmedNotes.length > 0; + const willBan = !isBanned; + const actionLabel = willBan ? "Ban" : "Unban"; + const ActionIcon = willBan ? IconBan : IconShieldCheck; + + const handleConfirm = async () => { + if (!isValid || isSubmitting) return; + setIsSubmitting(true); + setError(null); + const modApi = new ModerationApi(); + const resp = await modApi.ManageUserBan({ + username, + is_banned: willBan, + mod_notes: trimmedNotes, + }); + setIsSubmitting(false); + if (resp.success) { + setConfirmOpen(false); + onOpenChange(false); + onSuccess?.(); + } else { + setError(resp.errorMessage || "Failed to update ban status"); + setConfirmOpen(false); + } + }; + + return ( + <> + + + + + + {actionLabel} User + + + {willBan ? ( + <> + You are about to ban{" "} + + {displayName} + {" "} + (@{username}). They will lose access to their account. + + ) : ( + <> + You are about to unban{" "} + + {displayName} + {" "} + (@{username}). They will regain access to their account. + + )} + + + +
+
+ +