From 27d6794d22c5c19ccbe090aa55c14454be61880d Mon Sep 17 00:00:00 2001 From: BFlat Date: Sun, 31 Aug 2025 11:06:29 +0700 Subject: [PATCH] credits ui - topbar and settings modal (#804) - new progress bar and progress circle component - clicking the credits in the top opens the settings modal on the "Plans & Credits" tab --- .../src/components/signaled/TopBar/TopBar.tsx | 72 +++++++- .../libs/components/button/src/lib/button.tsx | 5 +- frontend/libs/components/progress/.babelrc | 12 ++ frontend/libs/components/progress/README.md | 7 + .../libs/components/progress/package.json | 17 ++ .../libs/components/progress/src/index.ts | 2 + .../progress/src/lib/progress-circle.tsx | 104 ++++++++++++ .../progress/src/lib/progress.module.css | 7 + .../progress/src/lib/progress.spec.tsx | 12 ++ .../components/progress/src/lib/progress.tsx | 42 +++++ .../libs/components/progress/tsconfig.json | 13 ++ .../components/progress/tsconfig.lib.json | 45 +++++ .../components/progress/tsconfig.spec.json | 34 ++++ .../libs/components/progress/vite.config.ts | 49 ++++++ .../promptbox/src/lib/PromptBoxEdit.tsx | 2 +- .../src/lib/panes/BillingSettingsPane.tsx | 100 +++++++++++ .../settings-modal/src/lib/settings-modal.tsx | 21 ++- frontend/package-lock.json | 159 +----------------- frontend/tsconfig.json | 3 + 19 files changed, 543 insertions(+), 163 deletions(-) create mode 100644 frontend/libs/components/progress/.babelrc create mode 100644 frontend/libs/components/progress/README.md create mode 100644 frontend/libs/components/progress/package.json create mode 100644 frontend/libs/components/progress/src/index.ts create mode 100644 frontend/libs/components/progress/src/lib/progress-circle.tsx create mode 100644 frontend/libs/components/progress/src/lib/progress.module.css create mode 100644 frontend/libs/components/progress/src/lib/progress.spec.tsx create mode 100644 frontend/libs/components/progress/src/lib/progress.tsx create mode 100644 frontend/libs/components/progress/tsconfig.json create mode 100644 frontend/libs/components/progress/tsconfig.lib.json create mode 100644 frontend/libs/components/progress/tsconfig.spec.json create mode 100644 frontend/libs/components/progress/vite.config.ts create mode 100644 frontend/libs/components/settings-modal/src/lib/panes/BillingSettingsPane.tsx diff --git a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx index 6de8ef9ebb..c4c791938f 100644 --- a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx +++ b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx @@ -35,7 +35,6 @@ import { import { signal } from "@preact/signals-react"; import { useSignals } from "@preact/signals-react/runtime"; import { setLogoutStates } from "~/signals/authentication/utilities"; - import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { TabId, useTabStore } from "~/pages/Stores/TabState"; import { @@ -43,7 +42,6 @@ import { is3DSceneLoaded, set3DPageMounted, } from "~/pages/PageEnigma/Editor/editor"; -// import { usePricingModalStore } from "@storyteller/ui-pricing-modal"; - Uncomment for pricing modal - BFlat import toast from "react-hot-toast"; import { gtagEvent } from "@storyteller/google-analytics"; import { @@ -55,12 +53,22 @@ import { BaseSelectorImage } from "../../../pages/PageEdit/BaseImageSelector"; import { ProviderSetupModal } from "@storyteller/provider-setup-modal"; import { ProviderBillingModal } from "@storyteller/provider-billing-modal"; import { usePricingModalStore } from "@storyteller/ui-pricing-modal"; +// import { ProgressCircle } from "@storyteller/ui-progress"; // Uncomment this for Credits indicator - BFlat interface Props { pageName: string; loginSignUpPressed: () => void; } +// Settings section type to match the SettingsModal component +type SettingsSection = + | "general" + | "accounts" + | "alerts" + | "about" + | "provider_priority" + | "billing"; + const SWITCHER_THROTTLE_TIME = 500; // milliseconds // NB: See `TabState` for the default tab. @@ -108,6 +116,18 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => { useSignals(); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); + const [settingsSection, setSettingsSection] = + useState("general"); + + // Credit state (replace with real data fetch) + const [credits, setCredits] = useState({ + used: 800, + total: 1000, + }); + + const creditsRemaining = credits.total - credits.used; + const creditsRemainingPercentage = (creditsRemaining / credits.total) * 100; + const isLowCredit = creditsRemainingPercentage < 20; // Less than 20% credits remaining const { isDesktop, isMaximized, minimize, toggleMaximize, close } = useTauriWindowControls(); @@ -119,6 +139,16 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => { gtagEvent("open_gallery_modal", { tab: tabStore.activeTabId }); }; + // Force recreation of the modal when switching to billing + const handleOpenBillingSettings = () => { + setIsSettingsModalOpen(false); + setTimeout(() => { + setSettingsSection("billing"); + setIsSettingsModalOpen(true); + gtagEvent("open_billing_settings"); + }, 50); + }; + const tabStore = useTabStore(); const is3DSceneReady = is3DSceneLoaded.value; @@ -263,33 +293,60 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
-
- {/* - Uncomment for pricing modal - BFlat */} +
+ {/* Uncomment this for Credits indicator - BFlat */} + {/* + + */} + + @@ -333,6 +390,7 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => { isOpen={isSettingsModalOpen} onClose={() => setIsSettingsModalOpen(false)} globalAccountLogoutCallback={() => setLogoutStates()} + initialSection={settingsSection} /> { /> - + ); }; diff --git a/frontend/libs/components/button/src/lib/button.tsx b/frontend/libs/components/button/src/lib/button.tsx index b67b0ef068..3b1eeb6926 100644 --- a/frontend/libs/components/button/src/lib/button.tsx +++ b/frontend/libs/components/button/src/lib/button.tsx @@ -14,7 +14,7 @@ export interface ButtonProps extends ButtonHTMLAttributes { iconClassName?: string; iconFlip?: boolean; htmlFor?: string; - variant?: "primary" | "secondary" | "action" | "destructive"; + variant?: "primary" | "secondary" | "action" | "destructive" | "ghost"; loading?: boolean; as?: "button" | "link"; href?: string; @@ -47,6 +47,9 @@ export const Button = ({ case "destructive": { return "bg-red/70 hover:bg-red/50 text-white focus-visible:outline-red"; } + case "ghost": { + return "bg-transparent hover:bg-white/[8%] text-white focus-visible:outline-primary-600"; + } case "primary": default: { return "bg-primary hover:bg-primary-400 text-white focus-visible:outline-primary-600"; diff --git a/frontend/libs/components/progress/.babelrc b/frontend/libs/components/progress/.babelrc new file mode 100644 index 0000000000..1ea870ead4 --- /dev/null +++ b/frontend/libs/components/progress/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/frontend/libs/components/progress/README.md b/frontend/libs/components/progress/README.md new file mode 100644 index 0000000000..5a3ad42ec5 --- /dev/null +++ b/frontend/libs/components/progress/README.md @@ -0,0 +1,7 @@ +# @storyteller/ui-progress + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test @storyteller/ui-progress` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/frontend/libs/components/progress/package.json b/frontend/libs/components/progress/package.json new file mode 100644 index 0000000000..665b3eb78b --- /dev/null +++ b/frontend/libs/components/progress/package.json @@ -0,0 +1,17 @@ +{ + "name": "@storyteller/ui-progress", + "version": "0.0.1", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + } +} diff --git a/frontend/libs/components/progress/src/index.ts b/frontend/libs/components/progress/src/index.ts new file mode 100644 index 0000000000..2f63fe2096 --- /dev/null +++ b/frontend/libs/components/progress/src/index.ts @@ -0,0 +1,2 @@ +export * from "./lib/progress"; +export * from "./lib/progress-circle"; diff --git a/frontend/libs/components/progress/src/lib/progress-circle.tsx b/frontend/libs/components/progress/src/lib/progress-circle.tsx new file mode 100644 index 0000000000..30a0d63727 --- /dev/null +++ b/frontend/libs/components/progress/src/lib/progress-circle.tsx @@ -0,0 +1,104 @@ +import { HTMLAttributes } from "react"; +import { twMerge } from "tailwind-merge"; + +interface ProgressCircleProps extends HTMLAttributes { + value?: number; + max?: number; + isLow?: boolean; + size?: "small" | "large"; + showPercentage?: boolean; + reverse?: boolean; +} + +export const ProgressCircle = ({ + value = 0, + max = 100, + isLow = false, + size = "large", + showPercentage = false, + reverse = false, + className, + ...props +}: ProgressCircleProps) => { + const percentage = Math.min(Math.max(0, value), max); + const displayPercentage = percentage; + + // Make small circles actually smaller + const radius = size === "small" ? 8 : 40; + const strokeWidth = size === "small" ? 4 : 6; + const circumference = 2 * Math.PI * radius; + + // Calculate stroke dashoffset differently based on direction + const strokeDashoffset = reverse + ? (percentage / 100) * circumference // Counter-clockwise + : circumference - (percentage / 100) * circumference; // Clockwise + + const viewBoxSize = (radius + strokeWidth) * 2; + + return ( +
+ {/* For small size, show percentage label outside to the right */} + {showPercentage && size === "small" && ( +
+ + {Math.round(displayPercentage)}% + +
+ )} + + + + + + + {/* For large size, show percentage in the center */} + {showPercentage && size === "large" && ( +
+ + {Math.round(displayPercentage)}% + +
+ )} +
+ ); +}; + +export default ProgressCircle; diff --git a/frontend/libs/components/progress/src/lib/progress.module.css b/frontend/libs/components/progress/src/lib/progress.module.css new file mode 100644 index 0000000000..d0246c74b9 --- /dev/null +++ b/frontend/libs/components/progress/src/lib/progress.module.css @@ -0,0 +1,7 @@ +/* + * Replace this with your own classes + * + * e.g. + * .container { + * } +*/ \ No newline at end of file diff --git a/frontend/libs/components/progress/src/lib/progress.spec.tsx b/frontend/libs/components/progress/src/lib/progress.spec.tsx new file mode 100644 index 0000000000..5dd153f958 --- /dev/null +++ b/frontend/libs/components/progress/src/lib/progress.spec.tsx @@ -0,0 +1,12 @@ +import { render } from '@testing-library/react'; + +import StorytellerUiProgress from './progress'; + +describe('StorytellerUiProgress', () => { + + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + +}); diff --git a/frontend/libs/components/progress/src/lib/progress.tsx b/frontend/libs/components/progress/src/lib/progress.tsx new file mode 100644 index 0000000000..286efc290f --- /dev/null +++ b/frontend/libs/components/progress/src/lib/progress.tsx @@ -0,0 +1,42 @@ +import { HTMLAttributes } from "react"; +import { twMerge } from "tailwind-merge"; + +interface ProgressProps extends HTMLAttributes { + value?: number; + max?: number; + isLow?: boolean; +} + +export const Progress = ({ + value = 0, + max = 100, + isLow = false, + className, + ...props +}: ProgressProps) => { + const percentage = Math.min(Math.max(0, value), max); + + return ( +
+
+
+ ); +}; + +export default Progress; diff --git a/frontend/libs/components/progress/tsconfig.json b/frontend/libs/components/progress/tsconfig.json new file mode 100644 index 0000000000..b3d19d8f5e --- /dev/null +++ b/frontend/libs/components/progress/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/frontend/libs/components/progress/tsconfig.lib.json b/frontend/libs/components/progress/tsconfig.lib.json new file mode 100644 index 0000000000..f0782e3dd8 --- /dev/null +++ b/frontend/libs/components/progress/tsconfig.lib.json @@ -0,0 +1,45 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "vite/client" + ], + "rootDir": "src", + "jsx": "react-jsx", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo" + }, + "exclude": [ + "out-tsc", + "dist", + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ], + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/frontend/libs/components/progress/tsconfig.spec.json b/frontend/libs/components/progress/tsconfig.spec.json new file mode 100644 index 0000000000..18b7de51b8 --- /dev/null +++ b/frontend/libs/components/progress/tsconfig.spec.json @@ -0,0 +1,34 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "jsx": "react-jsx" + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/frontend/libs/components/progress/vite.config.ts b/frontend/libs/components/progress/vite.config.ts new file mode 100644 index 0000000000..7c64b69bdd --- /dev/null +++ b/frontend/libs/components/progress/vite.config.ts @@ -0,0 +1,49 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; +import * as path from 'path'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/components/progress', + plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + outDir: './dist', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: '@storyteller/ui-progress', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es' as const] + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: ['react','react-dom','react/jsx-runtime'] + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + } + }, +})); diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx index a86a54636d..4d300102dd 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx @@ -176,7 +176,7 @@ export const PromptBoxEdit = ({