mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-28 12:32:51 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde547ad1c | ||
|
|
f0e8bec162 |
@@ -1,8 +0,0 @@
|
||||
image_width: 1024
|
||||
image_height: 1024
|
||||
#scheduler_steps: 15
|
||||
# NB(bt): It looks like we can get down to 2 sampling steps, perhaps ?
|
||||
scheduler_steps: 8
|
||||
scheduler_samples: 1
|
||||
cfg_scale: 15.0
|
||||
seed: 42
|
||||
@@ -1,51 +1,39 @@
|
||||
use tauri::{AppHandle, TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
|
||||
use tauri::window::Color;
|
||||
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::window::Color;
|
||||
use tauri::{AppHandle, TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
pub async fn setup_main_window(
|
||||
app: &AppHandle,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let win_builder =
|
||||
WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
||||
.title("Artcraft")
|
||||
//.decorations(false) // NB: This breaks Mac!
|
||||
WebviewWindowBuilder::new(app, MAIN_WINDOW_NAME, WebviewUrl::default())
|
||||
.title("ArtCraft")
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.background_color(Color(0, 0, 0, 0))
|
||||
.enable_clipboard_access()
|
||||
.inner_size(800.0, 600.0);
|
||||
.inner_size(2400.0, 1300.0);
|
||||
|
||||
// set transparent title bar only when building for macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
let win_builder = win_builder
|
||||
.decorations(true) // NB: Mac requires decorations. Tons of capabilities disappear without it.
|
||||
.decorations(true) // NB: Mac requires decorations. Tons of capabilities disappear otherwise.
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.hidden_title(true)
|
||||
.accept_first_mouse(true) // https://github.com/tauri-apps/tauri/issues/11605#issuecomment-2460112096
|
||||
.focusable(true)
|
||||
.focused(true);
|
||||
|
||||
let window = win_builder.build()?;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let win_builder = win_builder
|
||||
.decorations(false); // NB: This breaks Mac!
|
||||
|
||||
/*// set background color only when building for macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use cocoa::appkit::{NSColor, NSWindow};
|
||||
use cocoa::base::{id, nil};
|
||||
#[cfg(target_os = "windows")]
|
||||
let win_builder = win_builder
|
||||
.drag_and_drop(false); // TODO: Is this necessary on Windows?
|
||||
|
||||
let ns_window = window.ns_window().unwrap() as id;
|
||||
unsafe {
|
||||
let bg_color = NSColor::colorWithRed_green_blue_alpha_(
|
||||
nil,
|
||||
50.0 / 255.0,
|
||||
158.0 / 255.0,
|
||||
163.5 / 255.0,
|
||||
0.0,
|
||||
);
|
||||
ns_window.setBackgroundColor_(bg_color);
|
||||
}
|
||||
}*/
|
||||
let _window = win_builder.build()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -11,15 +11,6 @@
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "ArtCraft",
|
||||
"width": 2400,
|
||||
"height": 1300,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": true,
|
||||
"dragDropEnabled": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": {
|
||||
|
||||
@@ -11,15 +11,6 @@
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "ArtCraft",
|
||||
"width": 2400,
|
||||
"height": 1300,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false,
|
||||
"dragDropEnabled": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
BASE_API="http://localhost:3000"
|
||||
GOOGLE_API="http://localhost:3000/google"
|
||||
FUNNEL_API="http://localhost:3000/funnel"
|
||||
CDN_API="http://localhost:3000/cdn"
|
||||
GRAVATAR_API="https://www.gravatar.com"
|
||||
DEPLOY_PRIME_URL="http://localhost:5173"
|
||||
REACT_APP_PUBLIC_POSTHOG_KEY="phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
|
||||
REACT_APP_PUBLIC_POSTHOG_UI="https://us.i.posthog.com"
|
||||
CONTEXT="DEVELOPMENT"
|
||||
@@ -1,9 +0,0 @@
|
||||
BASE_API="https://api.storyteller.ai"
|
||||
GOOGLE_API="https://storage.googleapis.com"
|
||||
FUNNEL_API="https://style.storyteller.ai"
|
||||
CDN_API="https://cdn.storyteller.ai"
|
||||
GRAVATAR_API="https://www.gravatar.com"
|
||||
DEPLOY_PRIME_URL="https://studio.storyteller.ai"
|
||||
REACT_APP_PUBLIC_POSTHOG_KEY="phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
|
||||
REACT_APP_PUBLIC_POSTHOG_UI="https://us.i.posthog.com"
|
||||
CONTEXT="DEVELOPMENT"
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* This is intended to be a basic starting point for linting in your app.
|
||||
* It relies on recommended configs out of the box for simplicity, but you can
|
||||
* and should modify this configuration to best suit your team's needs.
|
||||
*/
|
||||
const SEVERITY = {
|
||||
OFF: 0,
|
||||
WARN: 1,
|
||||
ERROR: 2,
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
},
|
||||
|
||||
// Base config
|
||||
extends: ["eslint:recommended", "plugin:storybook/recommended"],
|
||||
|
||||
ignorePatterns: ["storybook-static/*"],
|
||||
|
||||
overrides: [
|
||||
// React
|
||||
{
|
||||
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||
plugins: ["react", "jsx-a11y"],
|
||||
extends: [
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:jsx-a11y/recommended",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
formComponents: ["Form"],
|
||||
linkComponents: [
|
||||
{ name: "Link", linkAttribute: "to" },
|
||||
{ name: "NavLink", linkAttribute: "to" },
|
||||
],
|
||||
"import/resolver": {
|
||||
typescript: {},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// we are incapable to provide support for blind people atm
|
||||
"jsx-a11y/click-events-have-key-events": SEVERITY.OFF,
|
||||
"jsx-a11y/no-static-element-interactions": SEVERITY.OFF,
|
||||
"jsx-a11y/media-has-caption": SEVERITY.OFF,
|
||||
"jsx-a11y/no-autofocus": SEVERITY.OFF,
|
||||
},
|
||||
},
|
||||
|
||||
// Typescript
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
plugins: ["@typescript-eslint", "import"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
settings: {
|
||||
"import/internal-regex": "^~/",
|
||||
"import/resolver": {
|
||||
node: {
|
||||
extensions: [".ts", ".tsx"],
|
||||
},
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
},
|
||||
|
||||
// Node
|
||||
{
|
||||
files: [".eslintrc.js"],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
# This action syncs "studio-staging" with "main" by force pushing every time it is run.
|
||||
# Adapted from:
|
||||
# https://github.community/t/user-github-pages-site-auto-deploy-from-another-branch-on-push/17912/2
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
sync_studio_staging_branch:
|
||||
runs-on: [ubuntu-latest]
|
||||
steps:
|
||||
- name: checkout main branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: sync main to studio-staging
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git checkout -b studio-staging
|
||||
- name: push changes
|
||||
uses: ad-m/github-push-action@0fafdd62b84042d49ec0cb92d9cac7f7ce4ec79e
|
||||
with:
|
||||
# Note: Github does not allow the default access token to push to
|
||||
# protected branches. Using a PAT with permissions can bypass this,
|
||||
# but it triggers recursive Github Actions calls on branch updates
|
||||
# (which the default access token does not). Ultimately, it's better
|
||||
# to leave the studio-staging branch unprotected.
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: studio-staging
|
||||
force: true
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.env
|
||||
.eslintcache
|
||||
.vite
|
||||
.notes
|
||||
node_modules
|
||||
/~/
|
||||
proxy/~/
|
||||
/.cache
|
||||
/public/build
|
||||
/build
|
||||
/backup
|
||||
.idea
|
||||
$
|
||||
|
||||
# VS Code Setting
|
||||
.vscode
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
# Mac
|
||||
.DS_Store
|
||||
|
||||
# Assets to ignore
|
||||
public/resources/movie/Big_Buck_Bunny_180_10s.webm
|
||||
public/resources/models/room/room_with_girl.glb
|
||||
|
||||
*storybook.log
|
||||
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
@@ -1,3 +0,0 @@
|
||||
@awesome.me:registry=https://npm.fontawesome.com/
|
||||
@fortawesome:registry=https://npm.fontawesome.com/
|
||||
//npm.fontawesome.com/:_authToken=104779DB-FD41-4BEA-BDB4-B1845D758A79
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"useTabs": false,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../**/*.mdx", "../**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-interactions",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: "sb-vite.config.ts",
|
||||
},
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
@@ -1,34 +0,0 @@
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
module.exports = function expressMiddleware(router) {
|
||||
router.use(
|
||||
"/v1",
|
||||
createProxyMiddleware({
|
||||
target: "https://api.storyteller.ai/v1",
|
||||
changeOrigin: true,
|
||||
on: {
|
||||
proxyReq: async (req, res) => {
|
||||
req.setHeader("Access-Control-Allow-Origin", "*");
|
||||
req.setHeader("Origin", "http://localhost:5173");
|
||||
req.setHeader("Referer", "http://localhost:5173");
|
||||
},
|
||||
},
|
||||
logger: console,
|
||||
}),
|
||||
);
|
||||
router.use(
|
||||
"/cdn-cgi",
|
||||
createProxyMiddleware({
|
||||
target: "https://cdn.storyteller.ai/cdn-cgi",
|
||||
changeOrigin: true,
|
||||
on: {
|
||||
proxyReq: async (req, res) => {
|
||||
req.setHeader("Access-Control-Allow-Origin", "*");
|
||||
req.setHeader("Origin", "http://localhost:5173");
|
||||
req.setHeader("Referer", "http://localhost:5173");
|
||||
},
|
||||
},
|
||||
logger: console,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
|
||||
Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +0,0 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "tailwindcss/tailwind.css";
|
||||
import "../app/styles/tailwind.css";
|
||||
import "../app/styles/normalize.css?url";
|
||||
import "../app//styles/tailwind.css?url";
|
||||
import "../app//styles/base.css?url";
|
||||
import { MemoryRouter } from "react-router";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>,
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -1,122 +0,0 @@
|
||||
# Storyteller GGF
|
||||
|
||||
## Coding Rules and Styles
|
||||
|
||||
```
|
||||
Short Style Guide TS React and Engine:
|
||||
|
||||
No use of lets
|
||||
|
||||
No basic for loop operators.
|
||||
Use the collection operators and array operators.
|
||||
|
||||
React Side / No modules over 200 lines. ( loose rule )
|
||||
|
||||
Don't do:
|
||||
if (condition) return;
|
||||
|
||||
Do:
|
||||
if (condition) {
|
||||
return;
|
||||
}
|
||||
|
||||
No use of any type.
|
||||
If you can use chat gpt to create the interface.
|
||||
```
|
||||
|
||||
## Branches
|
||||
|
||||
- `main` - The main branch is the production source of truth.
|
||||
|
||||
Changes to main will automatically build, but will not auto-deploy to production. You'll have to promote changes to production using the Netlify control panel.
|
||||
|
||||
Merge your code to main frequently. If you have to have a long-lived set of changes, ask yourself whether it might be better to feature flag your code off and
|
||||
land it to main rather than fight rebasing against the whole team.
|
||||
|
||||
- `studio-staging` - The staging auto-deploy branch.
|
||||
|
||||
You do not ever need to interact with this branch. Changes to `main` will automatically be pushed to `studio-staging` and auto-deployed to
|
||||
https://studio-staging.studio.storyteller.ai . You can test changes that have landed in `main` here before promoting them to production.
|
||||
|
||||
- `studio-testing` - The testing branch.
|
||||
|
||||
Anyone can push their changes to this branch for testing in a production-like environment. Since it is built and hosted on https://studio-testing.studio.storyteller.ai ,
|
||||
you won't face CORS issues, same-site cookie issues, or have to use a local development proxy. (This doesn't obviate the need for a development
|
||||
proxy for fast development iteration, but is a fantastic way to test your changes in a "production-like" environment.)
|
||||
|
||||
Force push to this branch by using `git push -f origin studio-testing`
|
||||
|
||||
## API Docs
|
||||
|
||||
Server API docs live here: https://storyteller-docs.netlify.app/
|
||||
|
||||
## Local Testing & Development
|
||||
|
||||
### Set up `.env` and the Proxy Server
|
||||
|
||||
Copy & paste the env settings from `.env-local-proxy` to `.env`, (if the file `.env` do not exist, simply create one at the root of this project's folder beside the other env settings files).
|
||||
|
||||
The settings from `.env-local-proxy` directs all requests to `localhost:3000`, this will relieve us from having to deal with CORS between various services.
|
||||
|
||||
Then, start the proxy server in the folder `/proxy`. Before running the proxy for the first time, run:
|
||||
|
||||
```
|
||||
npm i
|
||||
```
|
||||
|
||||
To start the proxy run:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
If the proxy fails, make sure your node version is 18.18. If you need to change the node version
|
||||
run `npm rebuild` once before re-running `npm run dev`
|
||||
|
||||
### Run the Code Locally
|
||||
|
||||
To setup and run this project's code locally:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## PostHog
|
||||
|
||||
PostHog is installed and working. To create a feature flag go to https://us.posthog.com/project/75284/feature_flags
|
||||
Add the feature flag for all users.
|
||||
|
||||
To check the feature flag in the app use code similar to this
|
||||
|
||||
```ts
|
||||
import { usePosthogFeatureFlag } from "~/hooks/usePosthogFeatureflag";
|
||||
import { FeatureFlags } from "~/enums";
|
||||
|
||||
const Component = () => {
|
||||
const flag = usePosthogFeatureFlag(FeaturesFlags.FLAG_NAME);
|
||||
// if flag is true, the feature shoud be executed & showed;
|
||||
// if flag is false, the feature should be disabled & hid;
|
||||
// for example
|
||||
return(
|
||||
<>
|
||||
{flag && <ComponentThatDependsOnTheFlag />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Other Documentation
|
||||
|
||||
Here is the Swagger documentation for the backend endpoints
|
||||
|
||||
- https://storyteller-docs.netlify.app/
|
||||
|
||||
This project uses Remix for routing and Tailwind for styling:
|
||||
|
||||
- [Remix Docs](https://remix.run/docs)
|
||||
- [Tailwind Docs](https://tailwindcss.com/docs)
|
||||
|
||||
On Netlify, we use functions:
|
||||
|
||||
- [Netlify Functions Overview](https://docs.netlify.com/functions/overview)
|
||||
@@ -1,3 +0,0 @@
|
||||
# This is to allow Storyteller Studio to use Web Workers to call the engine
|
||||
/*
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
@@ -1,38 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
export class AnalyticsApi extends ApiManager {
|
||||
public PostAnalytics({
|
||||
maybeLastAction,
|
||||
maybeLogToken,
|
||||
}: {
|
||||
maybeLastAction: string;
|
||||
maybeLogToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/analytics/log_session`;
|
||||
|
||||
const body = {
|
||||
maybe_last_action: maybeLastAction,
|
||||
maybe_log_token: maybeLogToken,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
maybe_last_action: string;
|
||||
maybe_log_token: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
log_token?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.log_token,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import environmentVariables from "../EnvironmentVariables";
|
||||
|
||||
type NonNullableObject<T extends object> = NonNullable<T>;
|
||||
|
||||
export interface ApiResponse<T, P = undefined> {
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
data?: T;
|
||||
pagination?: P;
|
||||
}
|
||||
|
||||
export class ApiManager {
|
||||
ApiTargets: Record<string, string> = {};
|
||||
|
||||
constructor() {
|
||||
// look at the .env file
|
||||
this.ApiTargets = {
|
||||
BaseApi: environmentVariables.values.BASE_API as string,
|
||||
GoggleApi: environmentVariables.values.GOOGLE_API as string,
|
||||
FunnelApi: environmentVariables.values.FUNNEL_API as string,
|
||||
CdnApi: environmentVariables.values.CDN_API as string,
|
||||
GravatarApi: environmentVariables.values.GRAVATAR_API as string,
|
||||
uploadApi: environmentVariables.values.UPLOAD_API_VIDEO as string,
|
||||
};
|
||||
}
|
||||
|
||||
public async fetch<B, T>(
|
||||
endpoint: string,
|
||||
{
|
||||
method,
|
||||
query,
|
||||
body,
|
||||
}: {
|
||||
method: string;
|
||||
query?: Record<string, string | boolean | number | undefined>;
|
||||
body?: B;
|
||||
},
|
||||
): Promise<T> {
|
||||
const queryInString =
|
||||
query &&
|
||||
Object.entries(query).reduce(
|
||||
(allOptions, [key, value]) => {
|
||||
if (!value) {
|
||||
return allOptions;
|
||||
}
|
||||
allOptions[key] = value.toString();
|
||||
return allOptions;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
const endpointWithQueries = queryInString
|
||||
? endpoint + "?" + new URLSearchParams(queryInString)
|
||||
: endpoint;
|
||||
|
||||
const bodyInString = JSON.stringify(body);
|
||||
|
||||
const response = await fetch(endpointWithQueries, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: bodyInString,
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
public async fetchMultipartFormData<T>(
|
||||
endpoint: string,
|
||||
{
|
||||
method,
|
||||
body,
|
||||
}: {
|
||||
method: string;
|
||||
body: FormData;
|
||||
},
|
||||
): Promise<T> {
|
||||
const response = await fetch(endpoint, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: body,
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
protected get<T>({
|
||||
endpoint,
|
||||
query,
|
||||
}: {
|
||||
endpoint: string;
|
||||
query?: Record<string, string | boolean | number | undefined>;
|
||||
}): Promise<T> {
|
||||
return this.fetch<null, T>(endpoint, { method: "GET", query });
|
||||
}
|
||||
|
||||
protected post<B, T>({
|
||||
endpoint,
|
||||
query,
|
||||
body,
|
||||
}: {
|
||||
endpoint: string;
|
||||
query?: Record<string, string | boolean | number | undefined>;
|
||||
body?: B;
|
||||
}): Promise<T> {
|
||||
return this.fetch<B, T>(endpoint, {
|
||||
method: "POST",
|
||||
query,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
protected delete<B, T>({
|
||||
endpoint,
|
||||
query,
|
||||
body,
|
||||
}: {
|
||||
endpoint: string;
|
||||
query?: Record<string, string | boolean | number | undefined>;
|
||||
body?: B;
|
||||
}): Promise<T> {
|
||||
return this.fetch<B, T>(endpoint, {
|
||||
method: "DELETE",
|
||||
query,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
protected async postForm<T>({
|
||||
endpoint,
|
||||
formRecord,
|
||||
uuid,
|
||||
blob,
|
||||
blobFileName,
|
||||
}: {
|
||||
endpoint: string;
|
||||
formRecord: Record<string, string>;
|
||||
uuid: string;
|
||||
blob?: Blob | File;
|
||||
blobFileName?: string;
|
||||
}): Promise<T> {
|
||||
const formData = new FormData();
|
||||
formData.append("uuid_idempotency_token", uuid);
|
||||
Object.entries(formRecord).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
if (blob && blobFileName) {
|
||||
formData.append("file", blob, blobFileName);
|
||||
} else if (blob) {
|
||||
formData.append("file", blob);
|
||||
}
|
||||
|
||||
console.log(`HELLO${formData}`);
|
||||
return this.fetchMultipartFormData<T>(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
|
||||
protected camelToSnakeCase(str: string) {
|
||||
return str.replace(/([a-z0])([A-Z])/g, "$1_$2").toLowerCase();
|
||||
}
|
||||
|
||||
protected parseQueryValues(
|
||||
params: Record<string, string | string[] | boolean | number | undefined>,
|
||||
): Record<string, string> {
|
||||
return Object.entries(params).reduce(
|
||||
(allParams, [key, value]) => {
|
||||
if (!value) {
|
||||
return allParams;
|
||||
}
|
||||
const snakeKey = this.camelToSnakeCase(key);
|
||||
if (Array.isArray(value)) {
|
||||
return { ...allParams, [snakeKey]: value.join(",") };
|
||||
}
|
||||
return { ...allParams, [snakeKey]: value.toString() };
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
protected parseBodyValues<T extends object, B extends object>(
|
||||
params: NonNullableObject<T>,
|
||||
): B {
|
||||
return Object.entries(params).reduce((allParams, [key, value]) => {
|
||||
if (!value) {
|
||||
return allParams;
|
||||
}
|
||||
const snakeKey = this.camelToSnakeCase(key);
|
||||
if (Array.isArray(value)) {
|
||||
return { ...allParams, [snakeKey]: value };
|
||||
}
|
||||
return { ...allParams, [snakeKey]: value };
|
||||
}, {} as B);
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { BetaKey } from "~/models";
|
||||
import { Pagination } from "~/pages/PageEnigma/models";
|
||||
|
||||
export class BetaKeysApi extends ApiManager {
|
||||
public CreateBetaKey({
|
||||
maybeNote,
|
||||
maybeReferrerUsername,
|
||||
numberOfKeys,
|
||||
uuidIdempotencyToken,
|
||||
}: {
|
||||
maybeNote?: string;
|
||||
maybeReferrerUsername?: string;
|
||||
numberOfKeys: number;
|
||||
uuidIdempotencyToken: string;
|
||||
}): Promise<ApiResponse<string[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/create`;
|
||||
|
||||
const body = {
|
||||
maybe_note: maybeNote,
|
||||
maybe_referrer_username: maybeReferrerUsername,
|
||||
number_of_keys: numberOfKeys,
|
||||
uuid_idempotency_token: uuidIdempotencyToken,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
maybe_note?: string;
|
||||
maybe_referrer_username?: string;
|
||||
number_of_keys: number;
|
||||
uuid_idempotency_token: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
beta_keys?: string[];
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.beta_keys,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListBetaKeys({
|
||||
sortAscending,
|
||||
pageSize,
|
||||
pageIndex,
|
||||
maybeReferrerUsername,
|
||||
onlyListRemaining,
|
||||
}: {
|
||||
sortAscending?: boolean;
|
||||
pageSize?: number;
|
||||
pageIndex?: number;
|
||||
maybeReferrerUsername?: string;
|
||||
onlyListRemaining?: boolean;
|
||||
}): Promise<ApiResponse<BetaKey[], Pagination>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/list`;
|
||||
|
||||
const query = {
|
||||
sort_ascending: sortAscending,
|
||||
page_size: pageSize,
|
||||
page_index: pageIndex,
|
||||
maybe_referrer_username: maybeReferrerUsername,
|
||||
only_list_remaining: onlyListRemaining,
|
||||
};
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
beta_keys?: BetaKey[];
|
||||
pagination?: Pagination;
|
||||
BadInput?: string;
|
||||
}>({ endpoint, query })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.beta_keys,
|
||||
pagination: response.pagination,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public RedeemBetaKey({
|
||||
betaKey,
|
||||
}: {
|
||||
betaKey: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/redeem`;
|
||||
|
||||
return this.post<
|
||||
{
|
||||
beta_key: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { beta_key: betaKey } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public UpdateBetaKeyIsDistributed({
|
||||
betaKey,
|
||||
isDistributed,
|
||||
}: {
|
||||
betaKey: string;
|
||||
isDistributed: boolean;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${betaKey}/distributed`;
|
||||
|
||||
return this.post<
|
||||
{
|
||||
is_distributed: boolean;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { is_distributed: isDistributed } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public UpdateBetaKeyNote({
|
||||
token,
|
||||
note,
|
||||
}: {
|
||||
token: string;
|
||||
note?: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${token}/note`;
|
||||
|
||||
return this.post<
|
||||
{
|
||||
note?: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { note } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { Subscription } from "~/models/Billing";
|
||||
import { LoyaltyProgram } from "~/enums";
|
||||
|
||||
export class BillingApi extends ApiManager {
|
||||
public async ListActiveSubscriptions(): Promise<
|
||||
ApiResponse<{
|
||||
active_subscriptions: Subscription[];
|
||||
maybe_loyalty_program?: LoyaltyProgram;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/billing/active_subscriptions`;
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
active_subscriptions?: Subscription[];
|
||||
maybe_loyalty_program?: LoyaltyProgram;
|
||||
error_message?: string;
|
||||
}>({ endpoint: endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: {
|
||||
active_subscriptions: response.active_subscriptions || [],
|
||||
maybe_loyalty_program: response.maybe_loyalty_program,
|
||||
},
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
export class CommentsApi extends ApiManager {
|
||||
public CreateComment({
|
||||
commentMarkdown,
|
||||
entityToken,
|
||||
entityType,
|
||||
uuidIdempotencyToken,
|
||||
}: {
|
||||
commentMarkdown: string;
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
uuidIdempotencyToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/new`;
|
||||
|
||||
const body = {
|
||||
comment_markdown: commentMarkdown,
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
uuid_idempotency_token: uuidIdempotencyToken,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
comment_markdown: string;
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
uuid_idempotency_token: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
comment_token?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.comment_token,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteComment({
|
||||
commentToken,
|
||||
}: {
|
||||
commentToken: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/delete/${commentToken}`;
|
||||
|
||||
return this.post<
|
||||
{ as_mod: boolean },
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { as_mod: true } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListCommentsByEntity({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<string[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/list/${entityType}/${entityToken}`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
comments: string[];
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.comments,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
export class EngineApi extends ApiManager {
|
||||
public async ConvertTbxToGltf({
|
||||
mediaFileToken,
|
||||
uuidIdempotencyToken,
|
||||
}: {
|
||||
mediaFileToken: string;
|
||||
uuidIdempotencyToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/conversion/enqueue_fbx_to_gltf`;
|
||||
|
||||
const body = {
|
||||
media_file_token: mediaFileToken,
|
||||
uuid_idempotency_token: uuidIdempotencyToken,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{ media_file_token: string; uuid_idempotency_token: string },
|
||||
{ success?: boolean; inference_job_token?: string; BadInput?: string }
|
||||
>({
|
||||
endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => {
|
||||
return {
|
||||
success: response.success ?? false,
|
||||
data: response.inference_job_token,
|
||||
errorMessage: response.BadInput,
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
export class FeaturedItemsApi extends ApiManager {
|
||||
public CreateFeaturedItem({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/create`;
|
||||
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteFeaturedItem({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/delete`;
|
||||
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
};
|
||||
|
||||
return this.delete<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public CheckFeaturedItem({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<boolean>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/is_featured/${entityType}/${entityToken}`;
|
||||
|
||||
return this.post<
|
||||
undefined,
|
||||
{
|
||||
success?: boolean;
|
||||
is_featured?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.is_featured,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { Job, JobState } from "~/models";
|
||||
|
||||
export class JobsApi extends ApiManager {
|
||||
public GetJobByToken({
|
||||
token,
|
||||
}: {
|
||||
token: string;
|
||||
}): Promise<ApiResponse<JobState>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${token}`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
state: JobState;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.state,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListJobs(): Promise<ApiResponse<JobState[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/batch`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
job_states: JobState[];
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.job_states,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListRecentJobs(): Promise<ApiResponse<Job[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
jobs: Job[];
|
||||
error_reason?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.jobs,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteJobByToken(jobToken: string): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${jobToken}`;
|
||||
|
||||
return this.delete<
|
||||
undefined,
|
||||
{
|
||||
success: boolean;
|
||||
}
|
||||
>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
import {
|
||||
MediaFile,
|
||||
MediaInfo,
|
||||
Pagination,
|
||||
PaginationInfinite,
|
||||
} from "~/pages/PageEnigma/models";
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { authentication } from "~/signals";
|
||||
import type { Property } from "csstype";
|
||||
type Visibility = Property.Visibility;
|
||||
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
FilterMediaClasses,
|
||||
FilterMediaType,
|
||||
} from "~/enums";
|
||||
|
||||
interface ListMediaQuery {
|
||||
sort_ascending?: boolean;
|
||||
page_size?: number;
|
||||
cursor?: string;
|
||||
cursor_is_reversed?: boolean;
|
||||
filter_media_classes?: FilterMediaClasses[];
|
||||
filter_media_type?: FilterMediaType[];
|
||||
filter_engine_categories?: FilterEngineCategories[];
|
||||
}
|
||||
|
||||
interface ListUserMediaQuery {
|
||||
sort_ascending?: boolean;
|
||||
page_size?: number;
|
||||
page_index?: number;
|
||||
filter_media_classes?: FilterMediaClasses[];
|
||||
filter_media_type?: FilterMediaType[];
|
||||
filter_engine_categories?: FilterEngineCategories[];
|
||||
}
|
||||
|
||||
interface SearchFeaturedMediaQuery {
|
||||
search_term: string;
|
||||
filter_media_classes?: FilterMediaClasses[];
|
||||
filter_media_type?: FilterMediaType[];
|
||||
filter_engine_categories?: FilterEngineCategories[];
|
||||
}
|
||||
|
||||
export class MediaFilesApi extends ApiManager {
|
||||
public async DeleteMediaFileByToken({
|
||||
mediaFileToken,
|
||||
asMod = true,
|
||||
setDelete = true,
|
||||
}: {
|
||||
mediaFileToken: string;
|
||||
asMod?: boolean;
|
||||
setDelete?: boolean;
|
||||
}): Promise<ApiResponse<MediaFile>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/file/${mediaFileToken}`;
|
||||
|
||||
const body = { as_mod: asMod, set_delete: setDelete };
|
||||
return await this.delete<
|
||||
{
|
||||
as_mod: boolean;
|
||||
set_delete: boolean;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async ListMediaFilesByTokens({
|
||||
mediaTokens,
|
||||
}: {
|
||||
mediaTokens: string[];
|
||||
}): Promise<ApiResponse<MediaFile[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/batch`;
|
||||
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
media_files: MediaFile[];
|
||||
error_reason?: string;
|
||||
}>({ endpoint, query: { tokens: mediaTokens.join(",") } })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.media_files,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public async GetMediaFileByToken({
|
||||
mediaFileToken,
|
||||
}: {
|
||||
mediaFileToken: string;
|
||||
}): Promise<ApiResponse<MediaFile>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/file/${mediaFileToken}`;
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
media_file: MediaFile;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.media_file,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async ListMediaFiles(
|
||||
query: ListMediaQuery,
|
||||
): Promise<ApiResponse<MediaInfo[], PaginationInfinite>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list`;
|
||||
const queryWithStrings = {
|
||||
...query,
|
||||
filter_media_classes: query.filter_media_classes
|
||||
? query.filter_media_classes.join(",")
|
||||
: undefined,
|
||||
filter_media_type: query.filter_media_type
|
||||
? query.filter_media_type.join(",")
|
||||
: undefined,
|
||||
filter_engine_categories: query.filter_engine_categories
|
||||
? query.filter_engine_categories.join(",")
|
||||
: undefined,
|
||||
};
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
results: MediaInfo[];
|
||||
pagination?: PaginationInfinite;
|
||||
}>({ endpoint, query: queryWithStrings })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.results ?? [],
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async ListFeaturedMediaFiles(
|
||||
query: ListMediaQuery,
|
||||
): Promise<ApiResponse<MediaInfo[], PaginationInfinite>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list_featured`;
|
||||
const queryWithStrings = {
|
||||
...query,
|
||||
filter_media_classes: query.filter_media_classes
|
||||
? query.filter_media_classes.join(",")
|
||||
: undefined,
|
||||
filter_media_type: query.filter_media_type
|
||||
? query.filter_media_type.join(",")
|
||||
: undefined,
|
||||
filter_engine_categories: query.filter_engine_categories
|
||||
? query.filter_engine_categories.join(",")
|
||||
: undefined,
|
||||
};
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
results: MediaInfo[];
|
||||
pagination: PaginationInfinite;
|
||||
}>({ endpoint, query: queryWithStrings })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.results,
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async ListUserMediaFiles(
|
||||
query: ListUserMediaQuery,
|
||||
): Promise<ApiResponse<MediaInfo[], Pagination>> {
|
||||
const userName = authentication.userInfo.value?.username;
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list/user/${userName}`;
|
||||
const queryWithStrings = {
|
||||
...query,
|
||||
include_user_uploads: true,
|
||||
filter_media_classes: query.filter_media_classes
|
||||
? query.filter_media_classes.join(",")
|
||||
: undefined,
|
||||
filter_media_type: query.filter_media_type
|
||||
? query.filter_media_type.join(",")
|
||||
: undefined,
|
||||
filter_engine_categories: query.filter_engine_categories
|
||||
? query.filter_engine_categories.join(",")
|
||||
: undefined,
|
||||
};
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
results: MediaInfo[];
|
||||
pagination?: Pagination;
|
||||
}>({ endpoint, query: queryWithStrings })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.results ?? [],
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async SearchFeaturedMediaFiles(
|
||||
query: SearchFeaturedMediaQuery,
|
||||
): Promise<ApiResponse<MediaInfo[], Pagination>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/search_featured`;
|
||||
const queryWithStrings = {
|
||||
search_term: query.search_term,
|
||||
filter_media_classes: query.filter_media_classes
|
||||
? query.filter_media_classes.join(",")
|
||||
: undefined,
|
||||
filter_media_type: query.filter_media_type
|
||||
? query.filter_media_type.join(",")
|
||||
: undefined,
|
||||
filter_engine_categories: query.filter_engine_categories
|
||||
? query.filter_engine_categories.join(",")
|
||||
: undefined,
|
||||
};
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
results: MediaInfo[];
|
||||
pagination: Pagination;
|
||||
}>({ endpoint, query: queryWithStrings })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.results,
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => ({
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
}));
|
||||
}
|
||||
|
||||
public async SearchUserMediaFiles(
|
||||
query: SearchFeaturedMediaQuery,
|
||||
): Promise<ApiResponse<MediaInfo[], Pagination>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/search_session`;
|
||||
const queryWithStrings = {
|
||||
search_term: query.search_term,
|
||||
filter_media_classes: query.filter_media_classes
|
||||
? query.filter_media_classes.join(",")
|
||||
: undefined,
|
||||
filter_media_type: query.filter_media_type
|
||||
? query.filter_media_type.join(",")
|
||||
: undefined,
|
||||
filter_engine_categories: query.filter_engine_categories
|
||||
? query.filter_engine_categories.join(",")
|
||||
: undefined,
|
||||
};
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
results: MediaInfo[];
|
||||
pagination: Pagination;
|
||||
}>({ endpoint, query: queryWithStrings })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.results,
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => ({
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
}));
|
||||
}
|
||||
|
||||
public async RenameMediaFileByToken({
|
||||
mediaToken,
|
||||
name,
|
||||
}: {
|
||||
mediaToken: string;
|
||||
name: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/rename/${mediaToken}`;
|
||||
const body = { name };
|
||||
|
||||
return await this.post<
|
||||
{ name: string },
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public async UpdateCoverImage({
|
||||
mediaFileToken,
|
||||
imageToken,
|
||||
}: {
|
||||
mediaFileToken: string;
|
||||
imageToken: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/cover_image/${mediaFileToken}`;
|
||||
return await this.post<
|
||||
{ cover_image_media_file_token: string },
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { cover_image_media_file_token: imageToken } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async UpdateVisibility({
|
||||
mediaFileToken,
|
||||
visibility,
|
||||
}: {
|
||||
mediaFileToken: string;
|
||||
visibility: Visibility;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/visibility/${mediaFileToken}`;
|
||||
return await this.post<
|
||||
{ creator_set_visibility: string },
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { creator_set_visibility: visibility } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { FilterEngineCategories, Visibility } from "~/enums";
|
||||
|
||||
export class MediaUploadApi extends ApiManager {
|
||||
private sessionToken: string = undefined;
|
||||
|
||||
private async getSessionTokenForUploadStudioShot() {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/session_token`;
|
||||
return await this.get<{
|
||||
maybe_signed_session: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => {
|
||||
return {
|
||||
success: true,
|
||||
data: response["maybe_signed_session"],
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async UploadStudioShot({
|
||||
maybe_title,
|
||||
uuid_idempotency_token,
|
||||
blob,
|
||||
fileName,
|
||||
maybe_visibility,
|
||||
}: {
|
||||
maybe_title?: string;
|
||||
uuid_idempotency_token: string;
|
||||
blob: Blob | null;
|
||||
fileName: string;
|
||||
maybe_visibility?: Visibility;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
// This function is a bit special it requires that
|
||||
const endpoint = `${this.ApiTargets.uploadApi}/v1/media_files/upload/studio_shot`;
|
||||
|
||||
if (!this.sessionToken) {
|
||||
console.log("Session Token");
|
||||
const response = await this.getSessionTokenForUploadStudioShot();
|
||||
if (response.success && response.data) {
|
||||
this.sessionToken = response.data;
|
||||
console.log(`Token ${this.sessionToken}`);
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: "Could not obtain session tokens.",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log(`Session Token: ${this.sessionToken}`);
|
||||
}
|
||||
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
maybe_title,
|
||||
maybe_frame_rate_fps: 60,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
};
|
||||
|
||||
const formRecord = Object.entries(options).reduce(
|
||||
(allOptions, [key, value]) => {
|
||||
if (value === undefined) {
|
||||
return allOptions;
|
||||
}
|
||||
return { ...allOptions, [key]: value.toString() };
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("uuid_idempotency_token", uuid_idempotency_token);
|
||||
Object.entries(formRecord).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
if (blob && fileName) {
|
||||
formData.append("file", blob, fileName);
|
||||
} else if (blob) {
|
||||
formData.append("file", blob);
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Session: this.sessionToken,
|
||||
},
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.media_file_token) {
|
||||
return { success: true, data: result.media_file_token };
|
||||
} else {
|
||||
return { success: false, errorMessage: "Failed to Get Media Token" };
|
||||
}
|
||||
}
|
||||
|
||||
private async Upload({
|
||||
endpoint,
|
||||
uuid,
|
||||
blob,
|
||||
fileName,
|
||||
options,
|
||||
}: {
|
||||
endpoint: string;
|
||||
blob: Blob | File;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
options: Record<string, string | number | undefined>;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const formRecord = Object.entries(options).reduce(
|
||||
(allOptions, [key, value]) => {
|
||||
if (value === undefined) {
|
||||
return allOptions;
|
||||
}
|
||||
return { ...allOptions, [key]: value.toString() };
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
return await this.postForm<{
|
||||
success: boolean;
|
||||
media_file_token?: string;
|
||||
BadInput?: string;
|
||||
}>({ endpoint, formRecord, blob, blobFileName: fileName, uuid })
|
||||
.then((response) => ({
|
||||
success: Boolean(response.success ?? false),
|
||||
data: response.media_file_token,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async UploadAudio({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
maybe_title?: string | undefined;
|
||||
maybe_visibility?: Visibility | undefined;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/audio`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadImage({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
maybe_title?: string | undefined;
|
||||
maybe_visibility?: Visibility | undefined;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/image`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
is_intermediate_system_file: "true",
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadNewEngineAsset({
|
||||
file,
|
||||
fileName,
|
||||
uuid,
|
||||
engine_category,
|
||||
maybe_animation_type,
|
||||
maybe_duration_millis,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
}: {
|
||||
file: File;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
engine_category: FilterEngineCategories;
|
||||
maybe_animation_type?: string;
|
||||
maybe_duration_millis?: number;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: Visibility;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_engine_asset`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
engine_category,
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
maybe_animation_type,
|
||||
maybe_duration_millis,
|
||||
};
|
||||
return this.Upload({ endpoint, blob: file, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadNewScene({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: Visibility;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_scene`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadNewVideo({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
maybe_style_name,
|
||||
maybe_scene_source_media_file_token,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: Visibility;
|
||||
maybe_style_name?: string;
|
||||
maybe_scene_source_media_file_token?: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_video`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
is_intermediate_system_file: "true",
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
maybe_style_name,
|
||||
maybe_scene_source_media_file_token,
|
||||
};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadPmx({
|
||||
file,
|
||||
fileName,
|
||||
uuid,
|
||||
engine_category,
|
||||
maybe_animation_type,
|
||||
maybe_duration_millis,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
}: {
|
||||
file: File;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
engine_category?: string;
|
||||
maybe_animation_type?: string;
|
||||
maybe_duration_millis?: number;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: Visibility;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/pmx`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
is_intermediate_system_file: "true",
|
||||
engine_category,
|
||||
maybe_animation_type,
|
||||
maybe_duration_millis,
|
||||
maybe_title,
|
||||
maybe_visibility,
|
||||
};
|
||||
return this.Upload({ endpoint, blob: file, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadSavedScene({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
mediaToken,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
mediaToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/saved_scene/${mediaToken}`;
|
||||
const options: Record<string, string | number | undefined> = {};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
// this is used to send the scene snapshot to the vst upload endpoint this creates a snapshot of the scene where in we can remix from the og.
|
||||
public async UploadSceneSnapshotMediaFileForm({
|
||||
blob,
|
||||
uuid,
|
||||
maybe_title, // title of the scene at the time
|
||||
maybe_scene_source_media_file_token, // original token that started it all
|
||||
}: {
|
||||
blob: Blob;
|
||||
uuid: string;
|
||||
maybe_title: string;
|
||||
maybe_scene_source_media_file_token: string | undefined;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/scene_snapshot`;
|
||||
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
maybe_title: maybe_title,
|
||||
maybe_scene_source_media_file_token: maybe_scene_source_media_file_token,
|
||||
};
|
||||
|
||||
const fileName = `${maybe_title}-${uuid}`;
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options }); // token comes back and send it VST
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { StatusAlert } from "~/models/StatusAlert";
|
||||
|
||||
export class MiscApi extends ApiManager {
|
||||
public GetStatusAlertCheck(): Promise<ApiResponse<StatusAlert>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/session`;
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
maybe_alert?: Omit<StatusAlert, "refresh_interval_millis">;
|
||||
refresh_interval_millis: number;
|
||||
}>({ endpoint: endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: {
|
||||
...response.maybe_alert,
|
||||
refresh_interval_millis: response.refresh_interval_millis,
|
||||
},
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.mesasge,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
export class ModerationApi extends ApiManager {
|
||||
public async UpdateFeatureFlagsByToken({
|
||||
usernameOrToken,
|
||||
flags,
|
||||
}: {
|
||||
usernameOrToken: string;
|
||||
flags: string[];
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/moderation/user_feature_flags/${usernameOrToken}`;
|
||||
|
||||
const body = {
|
||||
action: {
|
||||
AddFlags: {
|
||||
flags,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{ action: { AddFlags: { flags: string[] } } },
|
||||
{ success?: boolean; BadInput?: string }
|
||||
>({
|
||||
endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => ({
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { Prompts } from "~/pages/PageEnigma/models";
|
||||
|
||||
export class PromptsApi extends ApiManager {
|
||||
public GetPromptsByToken({
|
||||
token,
|
||||
}: {
|
||||
token: string;
|
||||
}): Promise<ApiResponse<Prompts>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/prompts/${token}`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
prompt: Prompts;
|
||||
error_reason?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.prompt,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { GenerateTtsAudioErrorType } from "~/pages/PageEnigma/enums";
|
||||
import { Visibility } from "~/enums";
|
||||
|
||||
export interface GenerateTtsAudioRequest {
|
||||
creator_set_visibility?: Visibility;
|
||||
uuid_idempotency_token: string;
|
||||
tts_model_token: string;
|
||||
inference_text: string;
|
||||
is_storyteller_demo?: boolean;
|
||||
}
|
||||
|
||||
export interface GenerateTtsAudioResponse {
|
||||
success?: boolean;
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
|
||||
export class TtsApi extends ApiManager {
|
||||
public async GenerateTtsAudio(request: GenerateTtsAudioRequest): Promise<
|
||||
ApiResponse<{
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/tts/inference`;
|
||||
|
||||
const body = {
|
||||
...request,
|
||||
...(request.creator_set_visibility
|
||||
? {}
|
||||
: { creator_set_visibility: Visibility.Public }),
|
||||
};
|
||||
|
||||
return this.post<GenerateTtsAudioRequest, GenerateTtsAudioResponse>({
|
||||
endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.success) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: response.BadInput,
|
||||
data: {},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
inference_job_token: response.inference_job_token,
|
||||
inference_job_token_type: response.inference_job_token_type,
|
||||
},
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: GenerateTtsAudioErrorType.UnknownError,
|
||||
data: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import {
|
||||
Pagination,
|
||||
UserBookmarkBatch,
|
||||
UserBookmarkByEntity,
|
||||
UserBookmarkByUser,
|
||||
} from "~/pages/PageEnigma/models";
|
||||
import { authentication } from "~/signals";
|
||||
|
||||
export enum ScopedEntityTypes {
|
||||
USER = "user",
|
||||
TTS_MODEL = "tts-model",
|
||||
TTS_RESULT = "tts-result",
|
||||
W2L_TEMPLATE = "w2l-template",
|
||||
W2L_RESULT = "w2l-result",
|
||||
MEDIA_FILE = "media_file",
|
||||
MODEL_WEIGHT = "model_weight",
|
||||
VOICE_CONVERSION_MODEL = "voice_conversion_model",
|
||||
ZS_VOICE = "z_voice",
|
||||
}
|
||||
|
||||
export enum ScopedWeightTypes {
|
||||
HIFIGAN_TTl2 = "hifigan_tt2",
|
||||
RVC_V2 = "rvc_v2",
|
||||
SD_1_5 = "sd_1.5",
|
||||
SDXL = "sdxl",
|
||||
SO_VITS_SVC = "so_vits_svc",
|
||||
TT2 = "tt2",
|
||||
LORA = "loRA",
|
||||
VALL_E = "vall_e",
|
||||
COMFY_UI = "comfy_ui",
|
||||
}
|
||||
|
||||
export enum ScopedMediaFileType {
|
||||
IMAGE_GENERATION = "image_generation",
|
||||
TEXT_TO_SPEECH = "text_to_speech",
|
||||
VOCODER = "vocoder",
|
||||
VOICE_CONVERSION = "voice_conversion",
|
||||
WORKFLOW_CONFIG = "workflow_config",
|
||||
}
|
||||
|
||||
interface ListUserBookmarksByUserRequest {
|
||||
username?: string;
|
||||
sort_ascending?: boolean;
|
||||
page_size?: number;
|
||||
page_index?: number;
|
||||
maybe_scoped_entity_type?: ScopedEntityTypes[];
|
||||
maybe_scoped_weight_type?: ScopedWeightTypes[];
|
||||
maybe_scoped_media_file_type?: ScopedMediaFileType[];
|
||||
}
|
||||
|
||||
export class UserBookmarksApi extends ApiManager {
|
||||
public CreateUserBookmark({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
new_bookmark_count_for_entity?: number;
|
||||
user_bookmark_token?: string;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/create`;
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
new_bookmark_count_for_entity?: number;
|
||||
user_bookmark_token: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: {
|
||||
new_bookmark_count_for_entity: response.new_bookmark_count_for_entity,
|
||||
user_bookmark_token: response.user_bookmark_token,
|
||||
},
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteUserBookmark({
|
||||
entityToken,
|
||||
}: {
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/delete/${entityToken}`;
|
||||
|
||||
return this.delete<
|
||||
{ as_mod: boolean },
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { as_mod: true } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListUserBookmarks(): Promise<ApiResponse<UserBookmarkBatch[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/batch`;
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
bookmarks?: UserBookmarkBatch[];
|
||||
BadInput?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.bookmarks,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListUserBookmarksByUser({
|
||||
username,
|
||||
sort_ascending,
|
||||
page_size,
|
||||
page_index,
|
||||
maybe_scoped_entity_type,
|
||||
maybe_scoped_weight_type,
|
||||
maybe_scoped_media_file_type,
|
||||
}: ListUserBookmarksByUserRequest): Promise<
|
||||
ApiResponse<UserBookmarkByUser[], Pagination>
|
||||
> {
|
||||
const user = username ?? authentication.userInfo.value?.username;
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${user}`;
|
||||
|
||||
const query = {
|
||||
sort_ascending,
|
||||
page_size,
|
||||
page_index,
|
||||
maybe_scoped_entity_type: maybe_scoped_entity_type
|
||||
? maybe_scoped_entity_type.join(",")
|
||||
: undefined,
|
||||
maybe_scoped_weight_type: maybe_scoped_weight_type
|
||||
? maybe_scoped_weight_type.join(",")
|
||||
: undefined,
|
||||
maybe_scoped_media_file_type: maybe_scoped_media_file_type
|
||||
? maybe_scoped_media_file_type.join(",")
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
pagination?: Pagination;
|
||||
results: UserBookmarkByUser[];
|
||||
}>({ endpoint, query })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.results,
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListUserBookmarksByEntity({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<UserBookmarkByEntity[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${entityType}/${entityToken}`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
user_bookmarks: UserBookmarkByEntity[];
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.user_bookmarks,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
export interface UserRating {
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
rating_value: string;
|
||||
}
|
||||
|
||||
export class UserRatingApi extends ApiManager {
|
||||
public ListUserRatings(): Promise<ApiResponse<UserRating[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/batch`;
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
ratings?: UserRating[];
|
||||
BadInput?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.ratings,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public PostUserRating({
|
||||
entityToken,
|
||||
entityType,
|
||||
ratingValue,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
ratingValue: string;
|
||||
}): Promise<ApiResponse<number>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/rate`;
|
||||
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
rating_value: ratingValue,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
rating_value: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
new_positive_rating_count_for_entity?: number;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.new_positive_rating_count_for_entity,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListUserRatingByEntity({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/view/${entityType}/${entityToken}`;
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
maybe_rating_value?: string;
|
||||
BadInput?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => {
|
||||
return {
|
||||
success: response.success ?? false,
|
||||
data: response.maybe_rating_value,
|
||||
errorMessage: response.BadInput,
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { UserInfo } from "~/models";
|
||||
|
||||
export class UsersApi extends ApiManager {
|
||||
public GetSession(): Promise<
|
||||
ApiResponse<{
|
||||
loggedIn: boolean;
|
||||
user?: UserInfo;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/session`;
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
logged_in: boolean;
|
||||
user?: UserInfo;
|
||||
error_message?: string;
|
||||
}>({ endpoint: endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: {
|
||||
loggedIn: response.logged_in,
|
||||
user: response.user,
|
||||
},
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.mesasge,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public GetUserProfile(username: string): Promise<
|
||||
ApiResponse<{
|
||||
user?: UserInfo;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user/${username}/profile`;
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
user?: UserInfo;
|
||||
error_message?: string;
|
||||
}>({ endpoint: endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: {
|
||||
user: response.user,
|
||||
},
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
error_message: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async Login({
|
||||
usernameOrEmail,
|
||||
password,
|
||||
}: {
|
||||
usernameOrEmail: string;
|
||||
password: string;
|
||||
}): Promise<ApiResponse<{ signedSession?: string }>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/login`;
|
||||
const body = {
|
||||
username_or_email: usernameOrEmail,
|
||||
password: password,
|
||||
};
|
||||
return await this.post<
|
||||
{ username_or_email: string; password: string },
|
||||
{
|
||||
success: boolean;
|
||||
signed_session?: string;
|
||||
error_message?: string;
|
||||
error_type?: string;
|
||||
}
|
||||
>({
|
||||
endpoint: endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: { signedSession: response.signed_session },
|
||||
errorMessage: response.error_message,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
error_message: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public Logout(): Promise<ApiResponse<null>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/logout`;
|
||||
return this.post<null, { success: boolean; error_message?: string }>({
|
||||
endpoint: endpoint,
|
||||
})
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async Signup({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
}: {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
}): Promise<ApiResponse<{ signedSession?: string }>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/create_account`;
|
||||
const body = {
|
||||
email_address: email,
|
||||
password,
|
||||
password_confirmation: passwordConfirmation,
|
||||
username,
|
||||
};
|
||||
return await this.post<
|
||||
{
|
||||
username: string;
|
||||
email_address: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
},
|
||||
{
|
||||
success: boolean;
|
||||
signed_session?: string;
|
||||
error_fields?: Record<string, string>;
|
||||
error_message?: string;
|
||||
error_type?: string;
|
||||
}
|
||||
>({
|
||||
endpoint: endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => {
|
||||
return {
|
||||
success: response.success,
|
||||
data: { signedSession: response.signed_session },
|
||||
errorMessage: Object.values(response.error_fields ?? {}).join(","),
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
error_message: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { Visibility } from "~/enums";
|
||||
|
||||
interface GenerateVideoStyleTransferRequest {
|
||||
creator_set_visibility: string;
|
||||
disable_lcm: boolean;
|
||||
enable_lipsync: boolean;
|
||||
frame_skip: number;
|
||||
global_ipa_media_token: string;
|
||||
input_depth_file: string;
|
||||
input_file: string;
|
||||
input_normal_file: string;
|
||||
input_outline_file: string;
|
||||
negative_prompt: string;
|
||||
prompt: string;
|
||||
remove_watermark: boolean;
|
||||
style: string;
|
||||
travel_prompt: string;
|
||||
trim_end_millis: number;
|
||||
trim_start_millis: number;
|
||||
use_cinematic: boolean;
|
||||
use_face_detailer: boolean;
|
||||
use_strength: number;
|
||||
use_upscaler: boolean;
|
||||
uuid_idempotency_token: string;
|
||||
}
|
||||
|
||||
export class VideoApi extends ApiManager {
|
||||
public async EnqueueStudio({
|
||||
enqueueVideo,
|
||||
}: {
|
||||
enqueueVideo: GenerateVideoStyleTransferRequest;
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/workflows/enqueue_studio`;
|
||||
|
||||
const body = {
|
||||
...enqueueVideo,
|
||||
...(enqueueVideo.creator_set_visibility
|
||||
? {}
|
||||
: { creator_set_visibility: Visibility.Public }),
|
||||
};
|
||||
|
||||
return await this.post<
|
||||
GenerateVideoStyleTransferRequest,
|
||||
{
|
||||
success?: boolean;
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: body })
|
||||
.then((response) => ({
|
||||
success: Boolean(response.success ?? false),
|
||||
data: {
|
||||
inference_job_token: response.inference_job_token,
|
||||
inference_job_token_type: response.inference_job_token_type,
|
||||
},
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
console.log(err.message);
|
||||
return { success: false, error_reason: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
interface VoiceConversionParams {
|
||||
autoPredictF0?: boolean;
|
||||
creatorSetVisibility?: string;
|
||||
isStorytellerDemo?: boolean;
|
||||
overrideF0Method?: string;
|
||||
sourceMediaUploadToken: string;
|
||||
transpose?: number;
|
||||
uuidIdempotencyToken: string;
|
||||
voiceConversionModelToken: string;
|
||||
}
|
||||
|
||||
interface VoiceConversionRequest {
|
||||
auto_predict_f0: boolean;
|
||||
creator_set_visibility: string;
|
||||
is_storyteller_demo: boolean;
|
||||
override_f0_method: string;
|
||||
source_media_upload_token: string;
|
||||
transpose: number;
|
||||
uuid_idempotency_token: string;
|
||||
voice_conversion_model_token: string;
|
||||
}
|
||||
|
||||
export class VoiceConversionApi extends ApiManager {
|
||||
public ConvertVoice(
|
||||
params: VoiceConversionParams,
|
||||
): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_conversion/inference`;
|
||||
|
||||
const body = this.parseBodyValues<
|
||||
VoiceConversionParams,
|
||||
VoiceConversionRequest
|
||||
>(params);
|
||||
|
||||
return this.post<
|
||||
VoiceConversionRequest,
|
||||
{
|
||||
success?: boolean;
|
||||
inference_job_token?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.inference_job_token,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { Pagination } from "~/pages/PageEnigma/models";
|
||||
import { authentication } from "~/signals";
|
||||
import { ZsDataset } from "~/models/Dataset";
|
||||
|
||||
interface VoiceDesignerParams {
|
||||
text: string;
|
||||
uuidIdempotencyToken: string;
|
||||
voiceToken: string;
|
||||
}
|
||||
|
||||
interface VoiceDesignerRequest {
|
||||
text: string;
|
||||
uuid_idempotency_token: string;
|
||||
voice_token: string;
|
||||
}
|
||||
|
||||
export class VoiceDesignerApi extends ApiManager {
|
||||
public EnqueueTts(
|
||||
params: VoiceDesignerParams,
|
||||
): Promise<
|
||||
ApiResponse<{ inference_job_token?: string; voice_token?: string }>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_designer/enqueue_tts`;
|
||||
|
||||
const body = this.parseBodyValues<
|
||||
VoiceDesignerParams,
|
||||
VoiceDesignerRequest
|
||||
>(params);
|
||||
|
||||
return this.post<
|
||||
VoiceDesignerRequest,
|
||||
{
|
||||
success?: boolean;
|
||||
inference_job_token?: string;
|
||||
voice_token: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
...(response.success
|
||||
? {
|
||||
data: {
|
||||
inference_job_token: response.inference_job_token,
|
||||
voice_token: response.voice_token,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListDatasetsByUser({
|
||||
username,
|
||||
}: {
|
||||
username?: string;
|
||||
}): Promise<ApiResponse<ZsDataset[]>> {
|
||||
const user = username ?? authentication.userInfo.value?.username;
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_designer/user/${user}/list`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
datasets: ZsDataset[];
|
||||
pagination: Pagination;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.datasets,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { authentication } from "~/signals";
|
||||
import { Weight } from "~/models";
|
||||
import { Pagination, PaginationInfinite } from "~/pages/PageEnigma/models";
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
FilterMediaClasses,
|
||||
FilterMediaType,
|
||||
Visibility,
|
||||
} from "~/enums";
|
||||
|
||||
export enum ScopedWeightType {
|
||||
HIFIGAN_TT2 = "hifigan_tt2",
|
||||
RVC_V2 = "rvc_v2",
|
||||
SD_1_5 = "sd_1.5",
|
||||
SDXL = "sdxl",
|
||||
SO_VITS_SVC = "so_vits_svc",
|
||||
TT2 = "tt2",
|
||||
LORA = "loRA",
|
||||
VALL_E = "vall_e",
|
||||
COMFY_UI = "comfy_ui",
|
||||
}
|
||||
export enum ScopedWeightCategory {
|
||||
IMAGE_GENERATION = "image_generation",
|
||||
TEXT_TO_SPEECH = "text_to_speech",
|
||||
VOCODER = "vocoder",
|
||||
VOICE_CONVERSION = "voice_conversion",
|
||||
WORKFLOW_CONFIG = "workflow_config",
|
||||
}
|
||||
|
||||
interface WeightsRequest {
|
||||
username?: string;
|
||||
pageSize?: number;
|
||||
weightType?: ScopedWeightType[];
|
||||
weightCategory?: ScopedWeightCategory[];
|
||||
//TODO: what is scoped??
|
||||
scopedWeightType?: ScopedWeightType[];
|
||||
scopedWeightCategory?: ScopedWeightCategory[];
|
||||
}
|
||||
|
||||
export interface ListWeightsByUserRequest extends WeightsRequest {
|
||||
pageIndex?: number;
|
||||
sortAscending?: boolean;
|
||||
}
|
||||
|
||||
export interface ListWeightsRequest extends WeightsRequest {
|
||||
cursor?: string;
|
||||
cursorIsReversed?: boolean;
|
||||
}
|
||||
|
||||
export interface ListFeaturedWeightsRequest {
|
||||
sortAscending?: boolean;
|
||||
pageSize?: number;
|
||||
cursor?: string;
|
||||
cursorIsReversed?: boolean;
|
||||
filterMediaClasses?: FilterMediaClasses[];
|
||||
filterMediaType?: FilterMediaType[];
|
||||
filterEngineCategories?: FilterEngineCategories[];
|
||||
}
|
||||
|
||||
export interface SearchWeightParams {
|
||||
ietfLanguageSubtag: string;
|
||||
searchTerm: string;
|
||||
weightCategory: ScopedWeightCategory;
|
||||
weightType: ScopedWeightType;
|
||||
}
|
||||
|
||||
export interface SearchWeightRequest {
|
||||
ietf_language_subtag: string;
|
||||
search_term: string;
|
||||
weight_category: string;
|
||||
weight_type: string;
|
||||
}
|
||||
|
||||
interface GetWeightByTokenResponse extends Weight {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateWeightByTokenParams {
|
||||
weightToken: string;
|
||||
coverImageMediaFileToken: string;
|
||||
descriptionMarkdown: string;
|
||||
title: string;
|
||||
visibility: Visibility;
|
||||
}
|
||||
export interface UpdateWeightByTokenRequest {
|
||||
cover_image_media_file_token: string;
|
||||
description_markdown: string;
|
||||
title: string;
|
||||
visibility: Visibility;
|
||||
}
|
||||
|
||||
export class WeightsApi extends ApiManager {
|
||||
public ListWeightsByUser({
|
||||
username,
|
||||
...params
|
||||
}: ListWeightsByUserRequest): Promise<ApiResponse<Weight[], Pagination>> {
|
||||
const user = username ?? authentication.userInfo.value?.username;
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/by_user/${user}`;
|
||||
|
||||
const query = this.parseQueryValues(params);
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
results: Weight[];
|
||||
pagination: Pagination;
|
||||
}>({ endpoint, query })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.results,
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListWeights({
|
||||
...params
|
||||
}: ListWeightsRequest): Promise<ApiResponse<Weight[], PaginationInfinite>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list`;
|
||||
|
||||
const query = this.parseQueryValues(params);
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
results: Weight[];
|
||||
pagination: PaginationInfinite;
|
||||
}>({ endpoint, query })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.results,
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListWeightsFeatured({
|
||||
...params
|
||||
}: ListFeaturedWeightsRequest): Promise<
|
||||
ApiResponse<Weight[], PaginationInfinite>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list_featured`;
|
||||
|
||||
const query = this.parseQueryValues(params);
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
results: Weight[];
|
||||
pagination: PaginationInfinite;
|
||||
}>({ endpoint, query })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.results,
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListWeightsPinned(): Promise<ApiResponse<Weight[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list_pinned`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
results: Weight[];
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.results,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public SearchWeights(
|
||||
params: SearchWeightParams,
|
||||
): Promise<ApiResponse<Weight[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/search`;
|
||||
|
||||
const body = this.parseBodyValues<SearchWeightParams, SearchWeightRequest>(
|
||||
params,
|
||||
);
|
||||
|
||||
return this.post<
|
||||
SearchWeightRequest,
|
||||
{
|
||||
success: boolean;
|
||||
weights: Weight[];
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.weights,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public GetWeightByToken({
|
||||
weightToken,
|
||||
}: {
|
||||
weightToken: string;
|
||||
}): Promise<ApiResponse<Weight>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`;
|
||||
|
||||
return this.get<GetWeightByTokenResponse>({ endpoint })
|
||||
.then(({ success, ...mediaFile }) => ({
|
||||
success: success,
|
||||
data: mediaFile,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public UpdateWeightByToken({
|
||||
weightToken,
|
||||
...params
|
||||
}: UpdateWeightByTokenParams): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`;
|
||||
|
||||
const body = this.parseBodyValues<
|
||||
Omit<UpdateWeightByTokenParams, "weightToken">,
|
||||
UpdateWeightByTokenRequest
|
||||
>(params);
|
||||
|
||||
return this.post<
|
||||
UpdateWeightByTokenRequest,
|
||||
{ success?: boolean; BadInput?: string }
|
||||
>({ endpoint, body })
|
||||
.then(({ success, BadInput }) => ({
|
||||
success: success ?? false,
|
||||
errorMessage: BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteWeightByToken({
|
||||
weightToken,
|
||||
}: {
|
||||
weightToken: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`;
|
||||
|
||||
const body = {
|
||||
as_mod: true,
|
||||
set_delete: true,
|
||||
};
|
||||
|
||||
return this.delete<
|
||||
{ as_mod: boolean; set_delete: boolean },
|
||||
{ success?: boolean; BadInput?: string }
|
||||
>({ endpoint, body })
|
||||
.then(({ success, BadInput }) => ({
|
||||
success: success ?? false,
|
||||
errorMessage: BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public UpdateWeightCoverImageByToken({
|
||||
weightToken,
|
||||
coverImageMediaFileToken,
|
||||
}: {
|
||||
weightToken: string;
|
||||
coverImageMediaFileToken: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/${weightToken}/cover_image`;
|
||||
|
||||
const body = {
|
||||
cover_image_media_file_token: coverImageMediaFileToken,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{ cover_image_media_file_token: string },
|
||||
{ success?: boolean; BadInput?: string }
|
||||
>({ endpoint, body })
|
||||
.then(({ success, BadInput }) => ({
|
||||
success: success ?? false,
|
||||
errorMessage: BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// https://storyteller-docs.netlify.app/#/
|
||||
export * from "./AnalyticsApi";
|
||||
export * from "./BetaKeysApi";
|
||||
export * from "./BillingApi";
|
||||
export * from "./CommentsApi";
|
||||
export * from "./EngineApi";
|
||||
export * from "./FeaturedItemsApi";
|
||||
export * from "./JobsApi";
|
||||
export * from "./MediaFilesApi";
|
||||
export * from "./MediaUploadApi";
|
||||
export * from "./MiscApi";
|
||||
export * from "./ModerationApi";
|
||||
export * from "./PromptsApi";
|
||||
export * from "./TtsApi";
|
||||
export * from "./UserBookmarksApi";
|
||||
export * from "./UsersApi";
|
||||
export * from "./VideoApi";
|
||||
export * from "./WeightsApi";
|
||||
@@ -1,10 +0,0 @@
|
||||
class EnvironmentVariables {
|
||||
values: Record<string, string | boolean> = {};
|
||||
|
||||
initialize(values: Record<string, string | boolean>) {
|
||||
this.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
const environmentVariables = new EnvironmentVariables();
|
||||
export default environmentVariables;
|
||||
@@ -1,57 +0,0 @@
|
||||
import environmentVariables from "~/Classes/EnvironmentVariables";
|
||||
|
||||
class BucketConfig {
|
||||
isLocalDev: boolean;
|
||||
|
||||
constructor() {
|
||||
this.isLocalDev =
|
||||
document.location.host.includes("localhost") ||
|
||||
document.location.host.includes("jungle.horse") ||
|
||||
document.location.host.startsWith("dev.");
|
||||
}
|
||||
|
||||
// TODO: Prevent callers with undefined/null paths
|
||||
// TODO pipeline works make more robust....
|
||||
getGcsUrl(bucketRelativePath: string | undefined | null): string {
|
||||
const bucket = this.getBucket();
|
||||
let path = bucketRelativePath;
|
||||
if (path !== undefined && path !== null && !path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
const media_api_base_url = environmentVariables.values.GOOGLE_API;
|
||||
return `${media_api_base_url}/${bucket}${path}`;
|
||||
}
|
||||
|
||||
private getBucket(): string {
|
||||
return this.isLocalDev ? "dev-vocodes-public" : "vocodes-public";
|
||||
}
|
||||
|
||||
getCdnUrl(
|
||||
bucketRelativePath: string,
|
||||
width?: number,
|
||||
quality?: number,
|
||||
): string {
|
||||
const cndBasePath = environmentVariables.values.CDN_API;
|
||||
const path = bucketRelativePath?.startsWith("/")
|
||||
? bucketRelativePath
|
||||
: "/" + bucketRelativePath;
|
||||
let resizeParams = "";
|
||||
if (width || quality) {
|
||||
resizeParams = "cdn-cgi/image/";
|
||||
if (width) {
|
||||
resizeParams += `width=${width},`;
|
||||
}
|
||||
if (quality) {
|
||||
resizeParams += `quality=${quality},`;
|
||||
}
|
||||
resizeParams = resizeParams.slice(0, -1);
|
||||
}
|
||||
if (resizeParams) {
|
||||
return `${cndBasePath}/${resizeParams}${path}`;
|
||||
} else {
|
||||
return `${cndBasePath}${path}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { BucketConfig };
|
||||
@@ -1,10 +0,0 @@
|
||||
export const apiHost = "https://api.storyteller.ai";
|
||||
|
||||
// Upload File
|
||||
export const uploadMedia = `${apiHost}/v1/media_files/upload`;
|
||||
|
||||
// Scenes
|
||||
export const uploadNewScene = `${apiHost}/v1/media_files/upload/new_scene`;
|
||||
export const uploadThumbnail = `${apiHost}/v1/media_files/cover_image/`;
|
||||
export const updateExistingScene = (sceneToken: string) =>
|
||||
`${apiHost}/v1/media_files/upload/saved_scene/${sceneToken}`;
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./reusable";
|
||||
export * from "./signaled";
|
||||
@@ -1,20 +0,0 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface BadgeProps {
|
||||
label: string;
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Badge = ({ label, className }: BadgeProps) => {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded-[6px] bg-ui-controls/45 px-[4px] py-[1px] text-[10px] shadow-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { Badge } from "./Badge";
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as ButtonStories from './Button.stories';
|
||||
|
||||
<Meta of={ButtonStories} />
|
||||
|
||||
# Button
|
||||
|
||||
A button denotes a possible action to be taken on behalf of the user.
|
||||
|
||||
There are three types of Button: primary, secondary, action.
|
||||
|
||||
<Canvas of={ButtonStories.Primary} />
|
||||
<Canvas of={ButtonStories.Secondary} />
|
||||
<Canvas of={ButtonStories.Action} />
|
||||
|
||||
It can be shown with an optional icon. If "iconFlip" is true, the icon shows after the "children", if false it shows before.
|
||||
|
||||
<Canvas of={ButtonStories.WithIconFlip} />
|
||||
<Canvas of={ButtonStories.WithIconNoFlip} />
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Button } from "./Button";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
import { faAngleLeft } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
component: Button,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["click"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
export const Primary: Story = {
|
||||
render: () => <Button variant="primary">Primary</Button>,
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
render: () => <Button variant="secondary">Secondary</Button>,
|
||||
};
|
||||
|
||||
export const Action: Story = {
|
||||
render: () => <Button variant="action">Action</Button>,
|
||||
};
|
||||
|
||||
export const WithIconFlip: Story = {
|
||||
render: () => (
|
||||
<Button variant="secondary" iconFlip icon={faAngleLeft}>
|
||||
Icon Flip
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIconNoFlip: Story = {
|
||||
render: () => (
|
||||
<Button variant="secondary" icon={faAngleLeft}>
|
||||
Icon
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -1,107 +0,0 @@
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon?: IconDefinition;
|
||||
iconClassName?: string;
|
||||
iconFlip?: boolean;
|
||||
htmlFor?: string;
|
||||
variant?: "primary" | "secondary" | "action";
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const Button = ({
|
||||
icon,
|
||||
iconClassName,
|
||||
children,
|
||||
className: propsClassName,
|
||||
htmlFor,
|
||||
variant: propsVariant = "primary",
|
||||
disabled,
|
||||
iconFlip = false,
|
||||
loading,
|
||||
...rest
|
||||
}: ButtonProps) => {
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "secondary": {
|
||||
return "bg-brand-secondary hover:bg-brand-secondary-900 text-white focus-visible:outline-brand-secondary";
|
||||
}
|
||||
case "action": {
|
||||
return " bg-action hover:bg-action-900 text-white focus-visible:outline-action ";
|
||||
}
|
||||
case "primary":
|
||||
default: {
|
||||
return "bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const disabledClass = twMerge(
|
||||
disabled || loading ? "opacity-50 pointer-events-none" : "",
|
||||
);
|
||||
|
||||
const className = twMerge(
|
||||
"text-sm font-medium rounded-lg px-3 py-2 border border-[#3F3F3F] shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-150 flex gap-2 items-center justify-center",
|
||||
getVariantClassNames(propsVariant),
|
||||
propsClassName,
|
||||
disabledClass,
|
||||
);
|
||||
|
||||
// const ButtonType = htmlFor ? "label" : "button";
|
||||
if (htmlFor) {
|
||||
return (
|
||||
<label className={className} htmlFor={htmlFor} style={rest.style}>
|
||||
{loading && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
{loading && iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
disabled={disabled || loading}
|
||||
{...{ ...rest, htmlFor }}
|
||||
>
|
||||
{loading && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
{loading && iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./Button";
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { TransitionDialogue, Button, ButtonProps } from "~/components";
|
||||
|
||||
type UnionedButtonProps = { label?: string } & ButtonProps;
|
||||
|
||||
interface ButtonDialoguePropsI {
|
||||
buttonProps?: UnionedButtonProps;
|
||||
confirmButtonProps?: UnionedButtonProps;
|
||||
closeButtonProps?: UnionedButtonProps;
|
||||
dialogProps?: {
|
||||
className?: string;
|
||||
};
|
||||
title?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
showClose?: boolean;
|
||||
}
|
||||
|
||||
export const ButtonDialogue = ({
|
||||
dialogProps = {},
|
||||
buttonProps: unionedButtonProps,
|
||||
confirmButtonProps,
|
||||
closeButtonProps: unionedCloseButtonProps,
|
||||
title,
|
||||
children,
|
||||
showClose = true,
|
||||
}: ButtonDialoguePropsI) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const closeModal = () => setIsOpen(false);
|
||||
const openModal = () => setIsOpen(true);
|
||||
|
||||
const { label: buttonLabel, ...buttonProps } = unionedButtonProps || {
|
||||
label: "Open",
|
||||
};
|
||||
const { label: closeButtonLabel, ...closeButtonProps } =
|
||||
unionedCloseButtonProps || { label: "Close" };
|
||||
return (
|
||||
<>
|
||||
<Button type="button" onClick={openModal} {...buttonProps}>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
|
||||
<TransitionDialogue
|
||||
title={title}
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
className={dialogProps.className}
|
||||
>
|
||||
<div className="mt-2 h-full">{children}</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
{showClose && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
{...closeButtonProps}
|
||||
variant="secondary"
|
||||
>
|
||||
{closeButtonLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{confirmButtonProps && (
|
||||
<Button
|
||||
type="button"
|
||||
{...confirmButtonProps}
|
||||
onClick={(e) => {
|
||||
if (confirmButtonProps.onClick) {
|
||||
confirmButtonProps.onClick(e);
|
||||
}
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
{confirmButtonProps.label ? confirmButtonProps.label : "Confirm"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { ButtonDialogue } from './ButtonDialogue';
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { Canvas, Meta } from "@storybook/blocks";
|
||||
|
||||
import * as ButtonDropdownStories from "./ButtonDropdown.stories";
|
||||
|
||||
<Meta of={ButtonDropdownStories} />
|
||||
|
||||
# Button for dropdown actions
|
||||
|
||||
Dropdown options that open up a dialog
|
||||
|
||||
<Canvas of={ButtonDropdownStories.OpenDialog} />
|
||||
|
||||
Dropdown options with onClick event
|
||||
|
||||
<Canvas of={ButtonDropdownStories.ClickEvent} />
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { ButtonDropdown } from "./ButtonDropdown";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
import { faAngleLeft, faFile } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
const meta: Meta<typeof ButtonDropdown> = {
|
||||
component: ButtonDropdown,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["click"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof ButtonDropdown>;
|
||||
|
||||
export const OpenDialog: Story = {
|
||||
render: () => (
|
||||
<ButtonDropdown
|
||||
label="Dialog Options"
|
||||
icon={faFile}
|
||||
options={[
|
||||
{
|
||||
label: "Open Dialog",
|
||||
description: "Desc 1",
|
||||
onDialogOpen: () => {
|
||||
//console.log("Opened Dialog");
|
||||
},
|
||||
dialogProps: {
|
||||
title: "Dialog Title",
|
||||
content: <div className="text-white">Dialog content here...</div>,
|
||||
confirmButtonProps: {
|
||||
label: "Create",
|
||||
disabled: false,
|
||||
onClick: () => {
|
||||
//console.log("Action on Dialog");
|
||||
},
|
||||
},
|
||||
closeButtonProps: {
|
||||
label: "Cancel",
|
||||
},
|
||||
showClose: true,
|
||||
onClose: () => {
|
||||
//console.log("Closed Dialog");
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Disabled",
|
||||
description: "Desc 2",
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const ClickEvent: Story = {
|
||||
render: () => (
|
||||
<ButtonDropdown
|
||||
label="Event Options"
|
||||
icon={faFile}
|
||||
options={[
|
||||
{
|
||||
label: "Click Event",
|
||||
description: "Desc",
|
||||
onClick: () => {
|
||||
//console.log("Clicked");
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
-212
@@ -1,212 +0,0 @@
|
||||
import { ButtonHTMLAttributes, Fragment, useState } from "react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import {
|
||||
IconDefinition,
|
||||
faChevronDown,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, ButtonProps, TransitionDialogue } from "~/components";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type UnionedButtonProps = { label?: string } & ButtonProps;
|
||||
|
||||
interface ButtonDropdownProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
label: string;
|
||||
icon?: IconDefinition;
|
||||
align?: "left" | "right";
|
||||
showSelected?: boolean;
|
||||
options: Array<{
|
||||
label: string;
|
||||
className?: string;
|
||||
icon?: IconDefinition;
|
||||
selected?: boolean;
|
||||
description?: string;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
onDialogOpen?: () => void;
|
||||
dialogProps?: {
|
||||
title: string;
|
||||
content: React.ReactNode;
|
||||
className?: string;
|
||||
confirmButtonProps?: UnionedButtonProps;
|
||||
closeButtonProps?: UnionedButtonProps;
|
||||
showClose?: boolean;
|
||||
onClose?: () => void;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export const ButtonDropdown = ({
|
||||
className,
|
||||
label,
|
||||
options,
|
||||
icon,
|
||||
align = "left",
|
||||
showSelected,
|
||||
}: ButtonDropdownProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedOptionIndex, setSelectedOptionIndex] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
options[selectedOptionIndex!].dialogProps?.onClose?.();
|
||||
};
|
||||
|
||||
const handleOptionClick = (index: number) => {
|
||||
const option = options[index];
|
||||
if (option.onClick) {
|
||||
option.onClick();
|
||||
}
|
||||
if (option.onDialogOpen) {
|
||||
option.onDialogOpen();
|
||||
}
|
||||
if (option.dialogProps) {
|
||||
setSelectedOptionIndex(index);
|
||||
setIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const currentDialogProps =
|
||||
selectedOptionIndex !== null
|
||||
? options[selectedOptionIndex].dialogProps
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Menu as="div" className="inline-block text-left">
|
||||
<Menu.Button as="div">
|
||||
<Button
|
||||
className={className}
|
||||
icon={faChevronDown}
|
||||
iconFlip={true}
|
||||
variant="secondary"
|
||||
>
|
||||
{icon ? <FontAwesomeIcon icon={icon} /> : null}
|
||||
{label}
|
||||
</Button>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
static
|
||||
className={twMerge(
|
||||
"absolute z-20 mt-1 w-max divide-y divide-gray-100 overflow-hidden rounded-lg bg-brand-secondary py-1.5 shadow-xl focus:outline-none",
|
||||
align === "left" ? "left-0" : "right-0",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{options.map((option, index) => (
|
||||
<Fragment key={index}>
|
||||
{option.divider && (
|
||||
<div className="my-1.5 border-t border-ui-divider" />
|
||||
)}
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
disabled={option.disabled}
|
||||
className={twMerge(
|
||||
"duration-50 bg-brand-secondary font-medium text-white transition-all",
|
||||
active ? "bg-brand-secondary-800" : "",
|
||||
option.disabled
|
||||
? "pointer-events-none opacity-40"
|
||||
: "",
|
||||
"group flex w-full items-center py-1.5 pl-7 pr-4 text-sm",
|
||||
option.className,
|
||||
)}
|
||||
onClick={() => handleOptionClick(index)}
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
{option.icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={option.icon}
|
||||
className="mr-2"
|
||||
/>
|
||||
)}
|
||||
<div className="grow text-start">{option.label}</div>
|
||||
<div className="ml-10 font-normal text-white/75">
|
||||
{option.description && option.description}
|
||||
</div>
|
||||
{showSelected && (
|
||||
<>
|
||||
{option.selected ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
className="ml-3 flex h-5"
|
||||
>
|
||||
<path
|
||||
opacity="1"
|
||||
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
|
||||
fill="#FC6B68"
|
||||
/>
|
||||
<path
|
||||
d="M369 175c-9.4 9.4-9.4 24.6 0 33.9L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<div className="w-8" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
|
||||
{currentDialogProps && (
|
||||
<TransitionDialogue
|
||||
title={currentDialogProps.title}
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
className={currentDialogProps.className}
|
||||
>
|
||||
{currentDialogProps.content}
|
||||
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
{currentDialogProps.showClose !== false &&
|
||||
currentDialogProps.closeButtonProps && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
{...currentDialogProps.closeButtonProps}
|
||||
onClick={closeModal}
|
||||
>
|
||||
{currentDialogProps.closeButtonProps.label}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentDialogProps.confirmButtonProps && (
|
||||
<Button
|
||||
{...currentDialogProps.confirmButtonProps}
|
||||
onClick={(e) => {
|
||||
if (currentDialogProps.confirmButtonProps?.onClick) {
|
||||
currentDialogProps.confirmButtonProps?.onClick(e);
|
||||
}
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
{currentDialogProps.confirmButtonProps.label || "Confirm"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./ButtonDropdown";
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as ButtonIconStories from './ButtonIcon.stories';
|
||||
|
||||
<Meta of={ButtonIconStories} />
|
||||
|
||||
# ButtonIcon
|
||||
|
||||
A button denotes a possible action to be taken on behalf of the user.
|
||||
|
||||
This button shows only a single icon.
|
||||
|
||||
<Canvas of={ButtonIconStories.Default} />
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { ButtonIcon } from "./ButtonIcon";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
import { faAngleLeft } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
const meta: Meta<typeof ButtonIcon> = {
|
||||
component: ButtonIcon,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["click"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof ButtonIcon>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <ButtonIcon icon={faAngleLeft} onClick={() => {}} />,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -1,39 +0,0 @@
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconProps,
|
||||
} from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
interface ButtonIconProps extends FontAwesomeIconProps {
|
||||
icon: IconDefinition;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
bgFill?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ButtonIcon = ({
|
||||
icon,
|
||||
size,
|
||||
onClick,
|
||||
className: propsClassName,
|
||||
bgFill = false,
|
||||
disabled,
|
||||
...rest
|
||||
}: ButtonIconProps) => {
|
||||
const className = twMerge(
|
||||
"box-content flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
||||
bgFill
|
||||
? "bg-ui-controls-button hover:bg-ui-controls-button/[0.75]"
|
||||
: "bg-transparent hover:bg-ui-panel/[0.4]",
|
||||
disabled && "opacity-50 hover:bg-transparent",
|
||||
propsClassName,
|
||||
);
|
||||
|
||||
return (
|
||||
<button className={className} onClick={onClick} disabled={disabled}>
|
||||
<FontAwesomeIcon icon={icon} size={size} {...rest} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./ButtonIcon";
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as ButtonIconSelectStories from './ButtonIconSelect.stories';
|
||||
|
||||
<Meta of={ButtonIconSelectStories} />
|
||||
|
||||
# ButtonIconSelectStories
|
||||
|
||||
A button denotes a possible action to be taken on behalf of the user.
|
||||
|
||||
This button shows only a single icon.
|
||||
|
||||
<Canvas of={ButtonIconSelectStories.Default} />
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { ButtonIconSelect } from "./ButtonIconSelect";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
import { faAngleLeft, faAngleRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
const meta: Meta<typeof ButtonIconSelect> = {
|
||||
component: ButtonIconSelect,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["click"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof ButtonIconSelect>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<ButtonIconSelect
|
||||
onOptionChange={() => {}}
|
||||
options={[
|
||||
{
|
||||
value: "V1",
|
||||
icon: faAngleLeft,
|
||||
text: "Select 1",
|
||||
},
|
||||
{
|
||||
value: "V2",
|
||||
icon: faAngleRight,
|
||||
text: "Select 2",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
icon: IconDefinition;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface ButtonIconSelectProps {
|
||||
options: Option[];
|
||||
onOptionChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function ButtonIconSelect({
|
||||
options,
|
||||
onOptionChange,
|
||||
}: ButtonIconSelectProps) {
|
||||
const [selectedOption, setSelectedOption] = useState<string>(
|
||||
options[0].value,
|
||||
);
|
||||
|
||||
const handleOptionChange = (value: string) => {
|
||||
setSelectedOption(value);
|
||||
if (onOptionChange) {
|
||||
onOptionChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex space-x-1">
|
||||
{options.map(({ value, icon, text }) => (
|
||||
<button
|
||||
key={value}
|
||||
className={twMerge(
|
||||
`flex h-8 items-center justify-center rounded-lg border text-sm transition-all duration-150`,
|
||||
text ? "h-auto w-auto gap-2 px-2.5 py-1" : "w-8",
|
||||
selectedOption === value
|
||||
? "border-brand-primary bg-ui-panel/[0.3]"
|
||||
: "border-transparent hover:bg-ui-panel/[0.4]",
|
||||
)}
|
||||
onClick={() => handleOptionChange(value)}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
{text && (
|
||||
<span className="text-nowrap text-sm font-medium">{text}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./ButtonIconSelect";
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as ButtonLinkStories from './ButtonLink.stories';
|
||||
|
||||
<Meta of={ButtonLinkStories} />
|
||||
|
||||
# ButtonLink
|
||||
|
||||
A button denotes a possible action to be taken on behalf of the user.
|
||||
|
||||
The button label comes via "children".
|
||||
|
||||
There are three types of Button: primary, secondary, action.
|
||||
|
||||
<Canvas of={ButtonLinkStories.Primary} />
|
||||
<Canvas of={ButtonLinkStories.Secondary} />
|
||||
|
||||
It can be shown with an optional icon.
|
||||
|
||||
<Canvas of={ButtonLinkStories.WithIcon} />
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { ButtonLink } from "./ButtonLink";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
import { faAngleLeft } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
const meta: Meta<typeof ButtonLink> = {
|
||||
component: ButtonLink,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["click"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof ButtonLink>;
|
||||
|
||||
export const Primary: Story = {
|
||||
render: () => (
|
||||
<ButtonLink to="google.com" variant="primary">
|
||||
Primary
|
||||
</ButtonLink>
|
||||
),
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
render: () => (
|
||||
<ButtonLink to="google.com" variant="secondary">
|
||||
Secondary
|
||||
</ButtonLink>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
render: () => (
|
||||
<ButtonLink to="google.com" variant="secondary" icon={faAngleLeft}>
|
||||
Icon
|
||||
</ButtonLink>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Link, LinkProps } from "@remix-run/react";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface LinkButtonProps extends LinkProps {
|
||||
icon?: IconDefinition;
|
||||
variant?: "primary" | "secondary";
|
||||
}
|
||||
|
||||
export const ButtonLink = ({
|
||||
icon,
|
||||
children,
|
||||
className: propsClassName,
|
||||
variant = "primary",
|
||||
...rest
|
||||
}: LinkButtonProps) => {
|
||||
//TODO: Duplicated from Button.tsx
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "secondary": {
|
||||
return "bg-brand-secondary hover:bg-brand-secondary-900 text-white focus-visible:outline-brand-secondary";
|
||||
}
|
||||
case "primary":
|
||||
default: {
|
||||
return "bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600";
|
||||
}
|
||||
}
|
||||
}
|
||||
const baseClassName =
|
||||
"text-sm font-medium whitespace-nowrap rounded-lg px-3 py-2 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-150";
|
||||
const variantClassNames = getVariantClassNames(variant);
|
||||
const className = twMerge(baseClassName, variantClassNames, propsClassName);
|
||||
// END TODO
|
||||
|
||||
return (
|
||||
<Link {...rest}>
|
||||
<button className={className}>
|
||||
{icon && <FontAwesomeIcon className="mr-2" icon={icon} size="sm" />}
|
||||
{children}
|
||||
</button>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./ButtonLink";
|
||||
@@ -1,111 +0,0 @@
|
||||
import { Fragment, useCallback } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { DoNotShow } from "~/constants";
|
||||
import { Button } from "~/components";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
title: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onOk?: () => void;
|
||||
okText?: string;
|
||||
okColor?: string;
|
||||
onCancel?: () => void;
|
||||
cancelText?: string;
|
||||
canHide?: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
text,
|
||||
title,
|
||||
open,
|
||||
onClose,
|
||||
onOk,
|
||||
okText = "OK",
|
||||
okColor = "bg-brand-success",
|
||||
onCancel,
|
||||
cancelText = "Cancel",
|
||||
canHide,
|
||||
}: Props) => {
|
||||
const handleOk = useCallback(() => {
|
||||
const element = document.getElementById("hide-dialog");
|
||||
if (element) {
|
||||
if ((element as HTMLInputElement).checked) {
|
||||
localStorage.setItem(title.replace(" ", "-"), DoNotShow);
|
||||
}
|
||||
}
|
||||
|
||||
onOk!();
|
||||
}, [onOk, title]);
|
||||
|
||||
return (
|
||||
<Transition appear show={open} as={Fragment}>
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/40" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel p-5 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h4"
|
||||
className="mb-4 text-xl font-bold text-white"
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
|
||||
<div className="mt-2">{text}</div>
|
||||
{canHide && (
|
||||
<div className="mt-2">
|
||||
<label>
|
||||
<input id="hide-dialog" type="checkbox" />
|
||||
Do not show this again
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
{!!onCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="rounded-lg px-3 py-2"
|
||||
variant="secondary"
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
{!!onOk && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleOk}
|
||||
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}
|
||||
>
|
||||
{okText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import { faFileArrowUp, faFileAudio } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { P } from "~/components";
|
||||
|
||||
export const DragAndDropZone = ({
|
||||
file,
|
||||
fileTypes,
|
||||
}: {
|
||||
file?: File;
|
||||
fileTypes: string[];
|
||||
}) => {
|
||||
if (!file) {
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center gap-3.5 rounded-lg border border-dashed border-[#3F3F3F] bg-brand-secondary p-3">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="text-4xl" />
|
||||
<div className="flex flex-col gap-0">
|
||||
<P className="font-medium">
|
||||
<u>Upload a file</u> or drop it here
|
||||
</P>
|
||||
|
||||
<P className="flex items-center gap-2 text-sm font-normal opacity-50">
|
||||
{fileTypes.join(", ").toString()} supported
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const fileName = file.name.split(".")[0].toUpperCase();
|
||||
const fileSize =
|
||||
file.size >= 1024 * 1024
|
||||
? (file.size / 1024 / 1024).toFixed(2) + " MB"
|
||||
: `${Math.floor(file.size / 1024)} KB`;
|
||||
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center justify-between gap-3.5 rounded-lg border border-dashed border-[#3F3F3F] bg-brand-secondary p-3">
|
||||
<FontAwesomeIcon icon={faFileAudio} className="text-4xl" />
|
||||
<div className="flex grow flex-col gap-0">
|
||||
<P className="font-medium">
|
||||
{file.name.slice(0, file.name.lastIndexOf("."))}
|
||||
</P>
|
||||
<P className="flex items-center gap-2 text-sm font-normal">
|
||||
<span className="opacity-50">
|
||||
{`${fileName} file size: ${fileSize} `}
|
||||
</span>
|
||||
<u className="transition-all hover:text-white/80">Change File</u>
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { FileUploader as DragDropFileUploader } from "react-drag-drop-files";
|
||||
// Usage refer to https://github.com/KarimMokhtar/react-drag-drop-files
|
||||
|
||||
import { DragAndDropZone } from "./DragAndDropZone";
|
||||
|
||||
export const FileUploader = ({
|
||||
file,
|
||||
fileTypes,
|
||||
handleChange,
|
||||
}: {
|
||||
file: File | undefined;
|
||||
handleChange: (file: File) => void;
|
||||
fileTypes: string[];
|
||||
}) => (
|
||||
<DragDropFileUploader
|
||||
handleChange={handleChange}
|
||||
name="file"
|
||||
maxSize={50}
|
||||
types={fileTypes}
|
||||
>
|
||||
<DragAndDropZone file={file} fileTypes={fileTypes} />
|
||||
</DragDropFileUploader>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./FileUploader";
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as FilterButtonsStories from './FilterButtons.stories';
|
||||
|
||||
<Meta of={FilterButtonsStories} />
|
||||
|
||||
# Three buttons which appear at the top of some of the media tabs.
|
||||
|
||||
The value can be set to Featured
|
||||
<Canvas of={FilterButtonsStories.Featured} />
|
||||
Mine
|
||||
<Canvas of={FilterButtonsStories.Mine} />
|
||||
or Bookmarked
|
||||
<Canvas of={FilterButtonsStories.Bookmarked} />
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FilterButtons } from "./FilterButtons";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
import { AssetFilterOption } from "~/enums";
|
||||
|
||||
const meta: Meta<typeof FilterButtons> = {
|
||||
component: FilterButtons,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["click"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof FilterButtons>;
|
||||
|
||||
export const Featured: Story = {
|
||||
render: () => (
|
||||
<FilterButtons onClick={() => null} value={AssetFilterOption.FEATURED} />
|
||||
),
|
||||
};
|
||||
|
||||
export const Mine: Story = {
|
||||
render: () => (
|
||||
<FilterButtons onClick={() => null} value={AssetFilterOption.MINE} />
|
||||
),
|
||||
};
|
||||
|
||||
export const Bookmarked: Story = {
|
||||
render: () => (
|
||||
<FilterButtons onClick={() => null} value={AssetFilterOption.BOOKMARKED} />
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -1,43 +0,0 @@
|
||||
import { AssetFilterOption } from "~/enums";
|
||||
|
||||
interface Props {
|
||||
onClick: (button: AssetFilterOption) => void;
|
||||
value: AssetFilterOption;
|
||||
}
|
||||
|
||||
const FILTERS = [
|
||||
{
|
||||
text: "Featured",
|
||||
option: AssetFilterOption.FEATURED,
|
||||
},
|
||||
{
|
||||
text: "Mine",
|
||||
option: AssetFilterOption.MINE,
|
||||
},
|
||||
{
|
||||
text: "Bookmarked",
|
||||
option: AssetFilterOption.BOOKMARKED,
|
||||
},
|
||||
];
|
||||
|
||||
export function FilterButtons({ onClick, value }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2 overflow-x-auto overflow-y-hidden px-4">
|
||||
{FILTERS.map((filter) => {
|
||||
const isBookmarks = filter.option === AssetFilterOption.BOOKMARKED;
|
||||
return (
|
||||
<button
|
||||
key={filter.option}
|
||||
className={`filter-tab${value === filter.option ? " active" : ""}`}
|
||||
disabled={isBookmarks}
|
||||
onClick={() => onClick(filter.option)}
|
||||
>
|
||||
{filter.text}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./FilterButtons";
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import EnvironmentVariables from "~/Classes/EnvironmentVariables";
|
||||
|
||||
interface Props {
|
||||
size: number;
|
||||
email_hash?: string;
|
||||
username?: string;
|
||||
avatarIndex?: number;
|
||||
backgroundIndex?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Gravatar(props: Props) {
|
||||
//url to the local fallback image
|
||||
const localDefaultUrl =
|
||||
props.avatarIndex === undefined
|
||||
? "/resources/avatars/0.webp"
|
||||
: `/resources/avatars/${props.avatarIndex}.webp`;
|
||||
|
||||
// submitted fallback need to be a url served from our server for gravatar to grab it
|
||||
const defaultImageUrl =
|
||||
props.avatarIndex === undefined
|
||||
? "https://storyteller.ai/images/avatars/2000x2000/0.webp"
|
||||
: `https://storyteller.ai/images/avatars/2000x2000/${props.avatarIndex}.webp`;
|
||||
|
||||
// NB: Gravatar suggests URI encoding these:
|
||||
// https://en.gravatar.com/site/implement/images/
|
||||
const encondedDefaultImage = encodeURIComponent(defaultImageUrl);
|
||||
|
||||
const gravatarUrl = `${EnvironmentVariables.values.GRAVATAR_API}/avatar/${props.email_hash}?s=${props.size}&d=${encondedDefaultImage}`;
|
||||
|
||||
const [{ imgUrl, showLoader }, setState] = useState<{
|
||||
imgUrl: string;
|
||||
showLoader: boolean;
|
||||
}>({
|
||||
imgUrl: gravatarUrl,
|
||||
showLoader: true,
|
||||
});
|
||||
|
||||
const altText = props.username ? `${props.username}'s gravatar` : "gravatar";
|
||||
|
||||
function getBackgroundColor(backgroundIndex?: number): string {
|
||||
switch (backgroundIndex) {
|
||||
case 0:
|
||||
return "#E66462";
|
||||
case 1:
|
||||
return "#FD881B";
|
||||
case 2:
|
||||
return "#E7C13C";
|
||||
case 3:
|
||||
return "#4BA905";
|
||||
case 4:
|
||||
return "#25B8A0";
|
||||
case 5:
|
||||
return "#0078D1";
|
||||
case 6:
|
||||
return "#7F52C1";
|
||||
case 7:
|
||||
return "#FF66AC";
|
||||
case 8:
|
||||
return "#259FEC";
|
||||
default:
|
||||
return `#1a1a27`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative aspect-square overflow-hidden rounded-full border border-white",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{showLoader && (
|
||||
<div className="absolute flex h-full w-full items-center justify-center bg-brand-secondary">
|
||||
<FontAwesomeIcon icon={faSpinnerThird} spin size={"lg"} />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
crossOrigin="anonymous"
|
||||
alt={altText}
|
||||
src={imgUrl}
|
||||
height={props.size}
|
||||
width={props.size}
|
||||
style={{ backgroundColor: getBackgroundColor(props.backgroundIndex) }}
|
||||
onLoad={() => {
|
||||
setState((curr) => ({
|
||||
imgUrl: curr.imgUrl,
|
||||
showLoader: false,
|
||||
}));
|
||||
}}
|
||||
onError={() => {
|
||||
setState((curr) => ({
|
||||
imgUrl:
|
||||
curr.imgUrl !== localDefaultUrl ? localDefaultUrl : curr.imgUrl,
|
||||
showLoader: false,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Gravatar };
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./Gravatar";
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as InputStories from './Input.stories';
|
||||
|
||||
<Meta of={InputStories} />
|
||||
|
||||
# Input control
|
||||
|
||||
Here is the input with placeholder
|
||||
<Canvas of={InputStories.Default} />
|
||||
|
||||
Here is the input with icon
|
||||
<Canvas of={InputStories.WithIcon} />
|
||||
|
||||
Here is the input with label
|
||||
<Canvas of={InputStories.WithLabel} />
|
||||
|
||||
Here is the input with error message
|
||||
<Canvas of={InputStories.WithError} />
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Input } from "./Input";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
import { faUser } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
component: Input,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["change", "blur", "focus"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof Input>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<div className="bg-action p-8">
|
||||
<Input placeholder="field placeholder" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
render: () => (
|
||||
<div className="bg-action p-8">
|
||||
<Input icon={faUser} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithLabel: Story = {
|
||||
render: () => (
|
||||
<div className="bg-action p-8">
|
||||
<Input label="Input label" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
render: () => (
|
||||
<div className="bg-action p-8">
|
||||
<Input errorMessage="Error message" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Label, H6 } from "~/components";
|
||||
import { kebabCase } from "~/utilities";
|
||||
import {
|
||||
disableHotkeyInput,
|
||||
enableHotkeyInput,
|
||||
DomLevels,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
inputClassName?: string;
|
||||
iconClassName?: string;
|
||||
label?: string;
|
||||
icon?: IconDefinition;
|
||||
isError?: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef(
|
||||
(
|
||||
{
|
||||
label,
|
||||
icon,
|
||||
inputClassName,
|
||||
iconClassName,
|
||||
className,
|
||||
id,
|
||||
isError,
|
||||
onBlur,
|
||||
onFocus,
|
||||
errorMessage,
|
||||
...rest
|
||||
}: InputProps,
|
||||
ref: React.ForwardedRef<HTMLInputElement>,
|
||||
) => {
|
||||
return (
|
||||
<div className={twMerge("flex flex-col", className)}>
|
||||
{label && <Label htmlFor={id ? id : kebabCase(label)}>{label}</Label>}
|
||||
|
||||
<div className="relative w-full">
|
||||
{icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
className={twMerge("text-md absolute pl-3 pt-3", iconClassName)}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
id={id ? id : label ? kebabCase(label) : undefined}
|
||||
className={twMerge(
|
||||
"h-10 w-full rounded-lg bg-brand-secondary px-3 py-2.5 text-white placeholder-white/50 outline-none",
|
||||
"border border-[#3F3F3F] transition-all duration-150 ease-in-out hover:border-brand-primary/60 focus:border-brand-primary focus:!outline-none",
|
||||
icon && "pl-10",
|
||||
isError && "outline-red focus:outline-red",
|
||||
inputClassName,
|
||||
)}
|
||||
onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (onFocus) {
|
||||
onFocus(e);
|
||||
}
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (onBlur) {
|
||||
onBlur(e);
|
||||
}
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<H6 className="absolute z-10 text-red">{errorMessage}</H6>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./Input";
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as InputVectorStories from './InputVector.stories';
|
||||
|
||||
<Meta of={InputVectorStories} />
|
||||
|
||||
# 3 Input controls: one for x, y, and z
|
||||
It allows entry os plus or minus numbers with decimals. It can also be adjusted via dragging the mouse left or right.
|
||||
|
||||
<Canvas of={InputVectorStories.Default} />
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { InputVector } from "./InputVector";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
|
||||
const meta: Meta<typeof InputVector> = {
|
||||
component: InputVector,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["change", "blur", "focus"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof InputVector>;
|
||||
|
||||
const InputVectorRender = () => {
|
||||
// Sets the hooks for both the label and primary props
|
||||
const [xyz, setXyz] = useState<Record<string, string>>({
|
||||
x: "0",
|
||||
y: "0",
|
||||
z: "0",
|
||||
});
|
||||
|
||||
return (
|
||||
<InputVector
|
||||
x={xyz.x}
|
||||
y={xyz.y}
|
||||
z={xyz.z}
|
||||
onChange={(value) => setXyz(value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<div className="bg-action p-8">
|
||||
<InputVectorRender />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -1,232 +0,0 @@
|
||||
import React, { ChangeEvent, RefObject, useRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
disableHotkeyInput,
|
||||
DomLevels,
|
||||
enableHotkeyInput,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
|
||||
interface InputVectorProps {
|
||||
x: string;
|
||||
y: string;
|
||||
z: string;
|
||||
onChange: (v: Record<string, string>) => void;
|
||||
increment?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const InputVector = ({
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
onChange,
|
||||
increment = 0.1,
|
||||
disabled,
|
||||
}: InputVectorProps) => {
|
||||
const xRef = useRef<HTMLInputElement>(null);
|
||||
const yRef = useRef<HTMLInputElement>(null);
|
||||
const zRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isDragging = useRef(false);
|
||||
const previousMousePosition = useRef(0);
|
||||
|
||||
const oldX = useRef("");
|
||||
const oldY = useRef("");
|
||||
const oldZ = useRef("");
|
||||
|
||||
oldX.current = x;
|
||||
oldY.current = y;
|
||||
oldZ.current = z;
|
||||
|
||||
const inputCommonClasses =
|
||||
"relative h-6 rounded-r-lg bg-brand-secondary p-2 text-sm text-white transition-all duration-100 ease-in-out outline-none -outline-offset-2 text-end w-full hover:cursor-e-resize hover:bg-brand-secondary-900";
|
||||
|
||||
const wrapperCommonClasses =
|
||||
"relative flex items-center before:inline-block before:h-6 before:bg-brand-primary before:text-white before:rounded-l-lg before:w-1.5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs";
|
||||
|
||||
const lockedCommonClasses =
|
||||
"hover:cursor-not-allowed hover:bg-brand-secondary";
|
||||
|
||||
function validateInput(newValue: string, oldValue: string) {
|
||||
if (newValue === "" || newValue === "-" || newValue === ".") {
|
||||
return newValue;
|
||||
}
|
||||
return /^-?\d*(\.\d{0,2})?$/.test(newValue) ? newValue : oldValue;
|
||||
}
|
||||
|
||||
function handleOnChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const newVector: Record<string, string> = {
|
||||
x: oldX.current,
|
||||
y: oldY.current,
|
||||
z: oldZ.current,
|
||||
};
|
||||
|
||||
newVector[event.target.name] = validateInput(
|
||||
event.target.value,
|
||||
newVector[event.target.name],
|
||||
);
|
||||
|
||||
onChange(newVector);
|
||||
}
|
||||
|
||||
const blurAllInputs = () => {
|
||||
xRef.current?.blur();
|
||||
yRef.current?.blur();
|
||||
zRef.current?.blur();
|
||||
};
|
||||
|
||||
// For dragging the input value to increment/decrement
|
||||
const handleMouseDown = (
|
||||
e: React.MouseEvent,
|
||||
ref: RefObject<HTMLInputElement>,
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
blurAllInputs();
|
||||
previousMousePosition.current = e.clientX;
|
||||
isDragging.current = true;
|
||||
|
||||
const mouseMoveHandler = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const currentMousePosition = e.clientX;
|
||||
const direction =
|
||||
currentMousePosition === previousMousePosition.current
|
||||
? 0
|
||||
: currentMousePosition > previousMousePosition.current
|
||||
? 1
|
||||
: -1;
|
||||
|
||||
previousMousePosition.current = currentMousePosition;
|
||||
|
||||
const newVector = {
|
||||
x: oldX.current,
|
||||
y: oldY.current,
|
||||
z: oldZ.current,
|
||||
} as Record<string, string>;
|
||||
|
||||
const newValue =
|
||||
parseFloat(newVector[ref.current?.name ?? ""]) + direction * increment;
|
||||
|
||||
newVector[ref.current?.name ?? ""] = (
|
||||
Math.round(newValue * 1000) / 1000
|
||||
).toString();
|
||||
|
||||
onChange(newVector);
|
||||
};
|
||||
|
||||
const mouseUpHandler = () => {
|
||||
document.removeEventListener("mousemove", mouseMoveHandler);
|
||||
document.removeEventListener("mouseup", mouseUpHandler);
|
||||
isDragging.current = false;
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", mouseMoveHandler);
|
||||
document.addEventListener("mouseup", mouseUpHandler);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
(e.currentTarget as HTMLInputElement).blur(); // Unfocus the input field on pressing Enter
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col justify-between gap-1.5">
|
||||
<span
|
||||
className={twMerge(
|
||||
wrapperCommonClasses,
|
||||
"before:bg-axis-x",
|
||||
disabled && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-3.5 z-10 font-semibold">X</div>
|
||||
<input
|
||||
className={twMerge(
|
||||
inputCommonClasses,
|
||||
"focus:outline-axis-x",
|
||||
disabled && lockedCommonClasses,
|
||||
)}
|
||||
type="text"
|
||||
name="x"
|
||||
onChange={handleOnChange}
|
||||
ref={xRef}
|
||||
value={x}
|
||||
disabled={disabled}
|
||||
onFocus={() => {
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onBlur={() => {
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
handleMouseDown(e, xRef);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={twMerge(
|
||||
wrapperCommonClasses,
|
||||
"before:bg-axis-y",
|
||||
disabled && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-3.5 z-10 font-semibold">Y</div>
|
||||
<input
|
||||
className={twMerge(
|
||||
inputCommonClasses,
|
||||
"focus:outline-axis-y",
|
||||
disabled && lockedCommonClasses,
|
||||
)}
|
||||
type="text"
|
||||
name="y"
|
||||
onChange={handleOnChange}
|
||||
ref={yRef}
|
||||
value={y}
|
||||
disabled={disabled}
|
||||
onFocus={() => {
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onBlur={() => {
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
handleMouseDown(e, yRef);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={twMerge(
|
||||
wrapperCommonClasses,
|
||||
"before:bg-axis-z",
|
||||
disabled && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-3.5 z-10 font-semibold">Z</div>
|
||||
<input
|
||||
className={twMerge(
|
||||
inputCommonClasses,
|
||||
"focus:outline-axis-z",
|
||||
disabled && lockedCommonClasses,
|
||||
)}
|
||||
type="text"
|
||||
name="z"
|
||||
onChange={handleOnChange}
|
||||
ref={zRef}
|
||||
value={z}
|
||||
disabled={disabled}
|
||||
onFocus={() => {
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onBlur={() => {
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
handleMouseDown(e, zRef);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./InputVector";
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { faCheck, faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
interface ListDropdownProps {
|
||||
list: { [key: string]: string }[];
|
||||
onSelect: (val: string) => void;
|
||||
}
|
||||
export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => {
|
||||
const [selected, setSelected] = useState(list[0]);
|
||||
|
||||
useEffect(() => {
|
||||
onSelect(Object.values(selected)[0]);
|
||||
}, [selected]);
|
||||
|
||||
return (
|
||||
<Listbox value={selected} onChange={setSelected}>
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button className="relative h-10 w-full cursor-pointer rounded-md bg-brand-secondary py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-all duration-150 ease-in-out focus:outline-brand-primary sm:text-sm">
|
||||
<span className="block truncate">{Object.keys(selected)[0]}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<FontAwesomeIcon icon={faChevronDown} aria-hidden="true" />
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-brand-secondary py-1 text-base shadow-lg focus:outline-none sm:text-sm">
|
||||
{list.map((item, itemIdx) => (
|
||||
<Listbox.Option
|
||||
key={itemIdx}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 text-white ${
|
||||
active ? "text-white" : "text-gray-400"
|
||||
}`
|
||||
}
|
||||
value={item}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate ${
|
||||
selected ? "font-medium" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{Object.values(item)[0]}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<FontAwesomeIcon icon={faCheck} aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { LoadingSpinner } from "~/components";
|
||||
|
||||
interface LoadingBarProps {
|
||||
id?: string;
|
||||
wrapperClassName?: string;
|
||||
innerWrapperClassName?: string;
|
||||
progressBackgroundClassName?: string;
|
||||
progressClassName?: string;
|
||||
variant?: string;
|
||||
show?: boolean;
|
||||
hasSpinner?: boolean;
|
||||
progressData: {
|
||||
progress: number;
|
||||
label?: string;
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
export const LoadingBar = ({
|
||||
wrapperClassName: propsWrapperClassName,
|
||||
innerWrapperClassName: propsInnerWrapperClassName,
|
||||
progressBackgroundClassName: propsProgressBackgroundClassName,
|
||||
progressClassName: propsProgressClassName,
|
||||
progressData,
|
||||
hasSpinner,
|
||||
variant = "primary",
|
||||
show = true,
|
||||
...rest
|
||||
}: LoadingBarProps) => {
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "secondary": {
|
||||
return " bg-brand-secondary text-white ";
|
||||
}
|
||||
case "primary":
|
||||
default: {
|
||||
return " bg-brand-primary text-white ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapperClassName = twMerge(
|
||||
"w-full h-full relative bg-ui-background",
|
||||
propsWrapperClassName,
|
||||
);
|
||||
const innerWrapperClassName = twMerge(
|
||||
"w-full h-full p-4 gap-6 m-auto flex flex-col justify-center items-center",
|
||||
propsInnerWrapperClassName,
|
||||
);
|
||||
const progressBackgroundClassName = twMerge(
|
||||
"w-full bg-brand-secondary-800 rounded-full h-2.5",
|
||||
propsProgressBackgroundClassName,
|
||||
);
|
||||
const progressClassName = twMerge(
|
||||
"h-2.5 rounded-full transition-all duration-1000",
|
||||
getVariantClassNames(variant),
|
||||
propsProgressClassName,
|
||||
);
|
||||
|
||||
return (
|
||||
<Transition
|
||||
className={wrapperClassName}
|
||||
show={show}
|
||||
enter="transition-opacity duration-150"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-1000"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
{...rest}
|
||||
>
|
||||
<div className={innerWrapperClassName}>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{hasSpinner && <LoadingSpinner className="h-5" />}
|
||||
{progressData.label && (
|
||||
<label className="text-lg font-medium">
|
||||
{progressData.label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={progressBackgroundClassName}>
|
||||
<div
|
||||
className={progressClassName}
|
||||
style={{ width: progressData.progress + "%" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{progressData.message && (
|
||||
<p className="text-center text-sm opacity-75">
|
||||
{progressData.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { H4 } from '.';
|
||||
|
||||
interface LoadingDotsProps {
|
||||
className?: string;
|
||||
isShowing?: boolean;
|
||||
};
|
||||
|
||||
interface LoadingDotsInnerProps {
|
||||
className?: string;
|
||||
isShowing?: boolean;
|
||||
type?: 'typing'|'bricks';
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export const LoadingDotsTyping = (props: LoadingDotsProps)=>{
|
||||
return (
|
||||
<LoadingDots {...props} />
|
||||
)
|
||||
}
|
||||
export const LoadingDotsBricks = (props: LoadingDotsProps)=>{
|
||||
return (
|
||||
<LoadingDots {...props} type="bricks"/>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoadingDots({
|
||||
className,
|
||||
isShowing=true,
|
||||
type = 'typing',
|
||||
message,
|
||||
}: LoadingDotsInnerProps){
|
||||
const classNames = twMerge("w-full h-full flex flex-col justify-center items-center bg-ui-background gap-6", className);
|
||||
|
||||
return(
|
||||
<Transition
|
||||
className={classNames}
|
||||
show={isShowing}
|
||||
enter="transition-opacity duration-150"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-1000"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{ type==='typing' &&
|
||||
<div className="dot-typing"></div>
|
||||
}
|
||||
{ type==='bricks' &&
|
||||
<div className="dot-bricks"></div>
|
||||
}
|
||||
{
|
||||
message &&
|
||||
<H4>{message}</H4>
|
||||
}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const LoadingSpinner = ({ className }: LoadingSpinnerProps) => {
|
||||
return (
|
||||
<svg
|
||||
className={twMerge("h-6 w-6 animate-spin", className)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Input } from "../Input";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface NumberInputProps {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export const NumberInput: React.FC<NumberInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState<string>(value.toString());
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(Math.round(value * 100) / 100 + "");
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = event.target.value;
|
||||
if (
|
||||
newValue === "" ||
|
||||
(!isNaN(Number(newValue)) && Number(newValue) <= 100)
|
||||
) {
|
||||
setInputValue(newValue);
|
||||
const numericValue = parseFloat(newValue);
|
||||
if (newValue !== "") {
|
||||
onChange(Math.round(numericValue * 100) / 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
if (inputValue === "") {
|
||||
setInputValue(Math.round(value * 100) / 100 + "");
|
||||
}
|
||||
};
|
||||
|
||||
const handleIncrement = () => {
|
||||
const newValue = Math.min(Number(inputValue) + 1, 100);
|
||||
setInputValue(Math.round(newValue * 100) / 100 + "");
|
||||
onChange(Math.round(newValue * 100) / 100);
|
||||
};
|
||||
|
||||
const handleDecrement = () => {
|
||||
const newValue = Math.max(Number(inputValue) - 1, 0);
|
||||
setInputValue(Math.round(newValue * 100) / 100 + "");
|
||||
onChange(Math.round(newValue * 100) / 100);
|
||||
};
|
||||
|
||||
const incrementButtonStyle =
|
||||
"flex h-[15px] w-6 items-center justify-center text-[10px] text-white/80 bg-ui-controls hover:bg-ui-controls-button outline-none focus:outline-none transition-all active:outline-none";
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
inputClassName="w-[70px] h-[30px] rounded-md text-sm"
|
||||
/>
|
||||
|
||||
<div className="absolute right-0 top-0 flex h-[30px] flex-col rounded-r-md border-l border-white/15 bg-gray-800">
|
||||
<button
|
||||
className={twMerge(
|
||||
incrementButtonStyle,
|
||||
"rounded-tr-md border-b border-white/10",
|
||||
)}
|
||||
onClick={handleIncrement}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronUp} />
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(incrementButtonStyle, "rounded-br-md")}
|
||||
onClick={handleDecrement}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./NumberInput";
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as PaginationStories from './Pagination.stories';
|
||||
|
||||
<Meta of={PaginationStories} />
|
||||
|
||||
# Controls for pagination.
|
||||
|
||||
Here is what it looks like when empty
|
||||
<Canvas of={PaginationStories.Empty} />
|
||||
First Page
|
||||
<Canvas of={PaginationStories.FirstPage} />
|
||||
Middle
|
||||
<Canvas of={PaginationStories.Middle} />
|
||||
Last Page
|
||||
<Canvas of={PaginationStories.LastPage} />
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Pagination } from "./Pagination";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
|
||||
const meta: Meta<typeof Pagination> = {
|
||||
component: Pagination,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["pageChange"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof Pagination>;
|
||||
|
||||
export const Empty: Story = {
|
||||
render: () => (
|
||||
<div className="w-[300px] bg-brand-secondary-200">
|
||||
<Pagination
|
||||
className=""
|
||||
currentPage={0}
|
||||
totalPages={0}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const FirstPage: Story = {
|
||||
render: () => (
|
||||
<div className="w-[300px] bg-brand-secondary-200">
|
||||
<Pagination
|
||||
className=""
|
||||
currentPage={0}
|
||||
totalPages={5}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const Middle: Story = {
|
||||
render: () => (
|
||||
<div className="w-[300px] bg-brand-secondary-200">
|
||||
<Pagination
|
||||
className=""
|
||||
currentPage={2}
|
||||
totalPages={5}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const LastPage: Story = {
|
||||
render: () => (
|
||||
<div className="w-[300px] bg-brand-secondary-200">
|
||||
<Pagination
|
||||
className=""
|
||||
currentPage={4}
|
||||
totalPages={5}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -1,99 +0,0 @@
|
||||
import {
|
||||
faChevronRight,
|
||||
faChevronLeft,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ButtonIcon } from "~/components";
|
||||
|
||||
export const Pagination = ({
|
||||
className,
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
onFetchMorePages,
|
||||
}: {
|
||||
className: string;
|
||||
currentPage: number; // index starts from 0
|
||||
totalPages: number;
|
||||
onPageChange: (newPage: number) => void;
|
||||
onFetchMorePages?: () => void;
|
||||
}) => {
|
||||
const pageGroupSize = 5;
|
||||
const pageGroup = Math.floor(currentPage / pageGroupSize);
|
||||
const totalPagesInPageGroup =
|
||||
(pageGroup + 1) * pageGroupSize > totalPages
|
||||
? totalPages % pageGroupSize
|
||||
: pageGroupSize;
|
||||
const pageGroupOffset = pageGroup * pageGroupSize;
|
||||
|
||||
const handleSetPreviousPage = () => {
|
||||
onPageChange(currentPage - 1);
|
||||
};
|
||||
const handleSetNextPage = () => {
|
||||
onPageChange(currentPage + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={twMerge(
|
||||
"flex items-center justify-between border-gray-200 px-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ButtonIcon
|
||||
className="-ml-2 pt-2 text-gray-400"
|
||||
icon={faChevronLeft}
|
||||
onClick={handleSetPreviousPage}
|
||||
disabled={currentPage === 0}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="hidden md:flex">
|
||||
{[...Array(totalPagesInPageGroup)].map((e, i) => {
|
||||
const pageIndex = i + pageGroupOffset;
|
||||
if (pageIndex === currentPage) {
|
||||
return (
|
||||
<button
|
||||
key={pageIndex}
|
||||
className="border-indigo-500 text-indigo-600 inline-flex items-center border-t-2 px-4 pt-2 text-sm font-medium"
|
||||
aria-current="page"
|
||||
onClick={() => onPageChange(pageIndex)}
|
||||
>
|
||||
{pageIndex + 1}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<button
|
||||
key={pageIndex}
|
||||
className="inline-flex items-center border-t-2 border-transparent px-4 pt-2 text-sm font-medium text-gray-500 hover:border-brand-primary hover:text-brand-primary"
|
||||
onClick={() => onPageChange(pageIndex)}
|
||||
>
|
||||
{pageIndex + 1}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<span>
|
||||
{currentPage + 1 !== totalPages ? (
|
||||
<ButtonIcon
|
||||
className="-mr-2 pt-2 text-gray-500 hover:text-brand-primary"
|
||||
icon={faChevronRight}
|
||||
onClick={handleSetNextPage}
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
onFetchMorePages && (
|
||||
<button
|
||||
className="inline-flex items-center border-t-2 border-transparent pl-2 pr-4 pt-2 text-sm font-medium text-gray-500 hover:text-brand-primary"
|
||||
key={totalPages}
|
||||
onClick={onFetchMorePages}
|
||||
>
|
||||
More...
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./Pagination";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as PillStories from './Pill.stories';
|
||||
|
||||
<Meta of={PillStories} />
|
||||
|
||||
# Pill
|
||||
|
||||
A pill is a container around the children.
|
||||
|
||||
<Canvas of={PillStories.Default} />
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Pill } from "./Pill";
|
||||
|
||||
const meta: Meta<typeof Pill> = {
|
||||
component: Pill,
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof Pill>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <Pill>A Pill</Pill>,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user