Merge branch 'main' into sentry-user
This commit is contained in:
commit
a21087ea25
276 changed files with 24827 additions and 17440 deletions
|
|
@ -50,6 +50,10 @@ GITHUB_CLIENT_ID=""
|
|||
GITHUB_CLIENT_SECRET=""
|
||||
BEEHIIVE_API_KEY=""
|
||||
BEEHIIVE_PUBLICATION_ID=""
|
||||
LISTMONK_DOMAIN=""
|
||||
LISTMONK_USER=""
|
||||
LISTMONK_API_KEY=""
|
||||
LISTMONK_LIST_ID=""
|
||||
THREADS_APP_ID=""
|
||||
THREADS_APP_SECRET=""
|
||||
FACEBOOK_APP_ID=""
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ name: Build
|
|||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
@ -16,6 +16,9 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
|
@ -47,3 +50,20 @@ jobs:
|
|||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Get Commit SHA (short)
|
||||
id: get_version
|
||||
run: |
|
||||
# Get the short 8-character commit SHA
|
||||
VERSION=$(git rev-parse --short=8 HEAD)
|
||||
echo "Commit SHA is $VERSION"
|
||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: SonarQube Analysis (Branch)
|
||||
uses: SonarSource/sonarqube-scan-action@v6
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.projectVersion=${{ steps.get_version.outputs.tag }}
|
||||
71
.github/workflows/build-pr
vendored
Normal file
71
.github/workflows/build-pr
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ['20.17.0']
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
environment:
|
||||
name: build-pr
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ env.STORE_PATH }}
|
||||
${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Get Commit SHA (short)
|
||||
id: get_version
|
||||
run: |
|
||||
# Get the short 8-character commit SHA
|
||||
VERSION=$(git rev-parse --short=8 HEAD)
|
||||
echo "Commit SHA is $VERSION"
|
||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: SonarQube Analysis (Pull Request)
|
||||
uses: SonarSource/sonarqube-scan-action@v6
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.projectVersion=${{ steps.get_version.outputs.tag }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
-Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }}
|
||||
-Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }}
|
||||
8
.github/workflows/pr-docker-build.yml
vendored
8
.github/workflows/pr-docker-build.yml
vendored
|
|
@ -9,10 +9,16 @@ permissions: write-all
|
|||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
environment:
|
||||
name: build-pr
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:20-alpine3.19
|
||||
FROM node:22.20-alpine
|
||||
ARG NEXT_PUBLIC_VERSION
|
||||
ENV NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION
|
||||
RUN apk add --no-cache g++ make py3-pip bash nginx
|
||||
|
|
|
|||
96
Jenkins/Build.Jenkinsfile
Normal file
96
Jenkins/Build.Jenkinsfile
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Declarative Pipeline for building Node.js application and running SonarQube analysis triggered by a push event.
|
||||
pipeline {
|
||||
// Defines the execution environment. Using 'agent any' to ensure an agent is available.
|
||||
agent any
|
||||
|
||||
// Global environment block removed to prevent Groovy scoping issues with manual path calculation.
|
||||
|
||||
stages {
|
||||
// Stage 1: Checkout the code (Relies on the initial SCM checkout done by Jenkins)
|
||||
stage('Source Checkout') {
|
||||
steps {
|
||||
echo "Workspace already populated by the initial SCM checkout. Proceeding."
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 2: Setup Node.js v20 and install pnpm
|
||||
stage('Setup Environment and Tools') {
|
||||
steps {
|
||||
sh '''
|
||||
echo "Ensuring required utilities and Node.js are installed..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl unzip nodejs
|
||||
|
||||
# 1. Install Node.js v20
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
echo "Node.js version: \$(node -v)"
|
||||
|
||||
# 2. Install pnpm globally (version 8)
|
||||
npm install -g pnpm@8
|
||||
echo "pnpm version: \$(pnpm -v)"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 3: Install dependencies and build the application
|
||||
stage('Install and Build') {
|
||||
steps {
|
||||
sh 'pnpm install'
|
||||
sh 'pnpm run build'
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 4: Run SonarQube analysis: Install scanner, get version, and execute.
|
||||
stage('SonarQube Analysis') {
|
||||
steps {
|
||||
script {
|
||||
// 1. Get the short 8-character commit SHA for project versioning
|
||||
def commitShaShort = sh(returnStdout: true, script: 'git rev-parse --short=8 HEAD').trim()
|
||||
echo "Commit SHA (short) is: ${commitShaShort}"
|
||||
|
||||
// --- 2. MANUALLY INSTALL THE SONAR SCANNER CLI LOCALLY IN THIS STAGE ---
|
||||
sh """
|
||||
echo "Manually downloading and installing Sonar Scanner CLI..."
|
||||
|
||||
# Download the stable scanner CLI package
|
||||
curl -sS -o sonar-scanner.zip \
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.7.0.2747.zip"
|
||||
|
||||
# Added -o flag to force overwrite and prevent interactive prompt failure
|
||||
unzip -o -q sonar-scanner.zip -d .
|
||||
"""
|
||||
|
||||
// 3. Find the extracted directory name and capture the full absolute bin path in Groovy
|
||||
// This is defined locally and used directly, avoiding environment variable issues.
|
||||
def scannerBinPath = sh(
|
||||
returnStdout: true,
|
||||
script: '''
|
||||
SCANNER_DIR=$(find . -maxdepth 1 -type d -name "sonar-scanner*" | head -n 1)
|
||||
# Get the full absolute path to the executable file
|
||||
echo \$(pwd)/\${SCANNER_DIR}/bin/sonar-scanner
|
||||
'''
|
||||
).trim()
|
||||
|
||||
echo "Scanner executable path captured: ${scannerBinPath}"
|
||||
|
||||
// 4. Use withSonarQubeEnv to set up the secure variables (HOST and TOKEN)
|
||||
withSonarQubeEnv(installationName: 'SonarQube-Server') {
|
||||
// 5. Execute the scanner using the Groovy variable directly.
|
||||
sh """
|
||||
echo "Starting SonarQube Analysis for project version: ${commitShaShort}"
|
||||
|
||||
# Execute the full, absolute path captured in the Groovy variable.
|
||||
'${scannerBinPath}' \\
|
||||
-Dsonar.projectVersion=${commitShaShort} \\
|
||||
-Dsonar.sources=.
|
||||
|
||||
# SONAR_HOST_URL and SONAR_TOKEN are automatically passed as environment variables
|
||||
# by the withSonarQubeEnv block.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
Jenkins/BuildPR.Jenkinsfile
Normal file
100
Jenkins/BuildPR.Jenkinsfile
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Declarative Pipeline for building Node.js application and running SonarQube analysis for a Pull Request.
|
||||
pipeline {
|
||||
// Defines the execution environment. Using 'agent any' to ensure an agent is available.
|
||||
agent any
|
||||
|
||||
// Environment variables that hold PR details, provided by Jenkins Multibranch setup.
|
||||
environment {
|
||||
// FIX: Environment variables must be quoted or wrapped in a function call.
|
||||
// We quote the 'env.CHANGE_ID' reference to fix the compilation error.
|
||||
PR_KEY = "${env.CHANGE_ID}"
|
||||
PR_BRANCH = "${env.CHANGE_BRANCH}"
|
||||
PR_BASE = "${env.CHANGE_TARGET}"
|
||||
}
|
||||
|
||||
stages {
|
||||
// Stage 1: Checkout the code (Relies on the initial SCM checkout done by Jenkins)
|
||||
stage('Source Checkout') {
|
||||
steps {
|
||||
echo "Workspace already populated by the initial SCM checkout. Proceeding."
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 2: Setup Node.js v20, install pnpm, and install required tools (curl, unzip)
|
||||
stage('Setup Environment and Tools') {
|
||||
steps {
|
||||
sh '''
|
||||
echo "Ensuring required utilities and Node.js are installed..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl unzip nodejs
|
||||
|
||||
# 1. Install Node.js v20 (closest matching the specified version '20.17.0')
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
echo "Node.js version: \$(node -v)"
|
||||
|
||||
# 2. Install pnpm globally (version 8)
|
||||
npm install -g pnpm@8
|
||||
echo "pnpm version: \$(pnpm -v)"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 3: Install dependencies and build the application
|
||||
stage('Install and Build') {
|
||||
steps {
|
||||
sh 'pnpm install'
|
||||
sh 'pnpm run build'
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 4: Run SonarQube PR analysis: Install scanner locally, get version, and execute.
|
||||
stage('SonarQube Pull Request Analysis') {
|
||||
steps {
|
||||
script {
|
||||
// 1. Get the short 8-character commit SHA for project versioning
|
||||
def commitShaShort = sh(returnStdout: true, script: 'git rev-parse --short=8 HEAD').trim()
|
||||
echo "Commit SHA (short) is: ${commitShaShort}"
|
||||
|
||||
// --- 2. MANUALLY INSTALL THE SONAR SCANNER CLI LOCALLY ---
|
||||
sh """
|
||||
echo "Manually downloading and installing Sonar Scanner CLI..."
|
||||
curl -sS -o sonar-scanner.zip \
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.7.0.2747.zip"
|
||||
unzip -o -q sonar-scanner.zip -d .
|
||||
"""
|
||||
|
||||
// 3. Find the extracted directory name and capture the full absolute executable path.
|
||||
def scannerBinPath = sh(
|
||||
returnStdout: true,
|
||||
script: '''
|
||||
SCANNER_DIR=$(find . -maxdepth 1 -type d -name "sonar-scanner*" | head -n 1)
|
||||
# Get the full absolute path to the executable file
|
||||
echo \$(pwd)/\${SCANNER_DIR}/bin/sonar-scanner
|
||||
'''
|
||||
).trim()
|
||||
|
||||
echo "Scanner executable path captured: ${scannerBinPath}"
|
||||
|
||||
// 4. Use withSonarQubeEnv to set up the secure variables (HOST and TOKEN)
|
||||
withSonarQubeEnv(installationName: 'SonarQube-Server') {
|
||||
// 5. Execute the scanner using the Groovy variable directly with PR parameters.
|
||||
sh """
|
||||
echo "Starting SonarQube Pull Request Analysis for PR #${PR_KEY}"
|
||||
|
||||
'${scannerBinPath}' \\
|
||||
-Dsonar.projectVersion=${commitShaShort} \\
|
||||
-Dsonar.sources=. \\
|
||||
-Dsonar.pullrequest.key=${PR_KEY} \\
|
||||
-Dsonar.pullrequest.branch=${PR_BRANCH} \\
|
||||
-Dsonar.pullrequest.base=${PR_BASE}
|
||||
|
||||
# SONAR_HOST_URL and SONAR_TOKEN are automatically passed as environment variables
|
||||
# by the withSonarQubeEnv block.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Jenkinsfile
vendored
70
Jenkinsfile
vendored
|
|
@ -1,70 +0,0 @@
|
|||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
NODE_VERSION = '20.17.0'
|
||||
PR_NUMBER = "${env.CHANGE_ID}" // PR number comes from webhook payload
|
||||
IMAGE_TAG="ghcr.io/gitroomhq/postiz-app-pr:${env.CHANGE_ID}"
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout Repository') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Check Node.js and npm') {
|
||||
steps {
|
||||
script {
|
||||
sh "node -v"
|
||||
sh "npm -v"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Install Dependencies') {
|
||||
steps {
|
||||
sh 'npm ci'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Project') {
|
||||
steps {
|
||||
sh 'npm run build'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build and Push Docker Image') {
|
||||
when {
|
||||
expression { return env.CHANGE_ID != null } // Only run if it's a PR
|
||||
}
|
||||
steps {
|
||||
withCredentials([string(credentialsId: 'gh-pat', variable: 'GITHUB_PASS')]) {
|
||||
// Docker login step
|
||||
sh '''
|
||||
echo "$GITHUB_PASS" | docker login ghcr.io -u "egelhaus" --password-stdin
|
||||
'''
|
||||
// Build Docker image
|
||||
sh '''
|
||||
docker build -f Dockerfile.dev -t $IMAGE_TAG .
|
||||
'''
|
||||
// Push Docker image to GitHub Container Registry
|
||||
sh '''
|
||||
docker push $IMAGE_TAG
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
success {
|
||||
echo 'Build completed successfully!'
|
||||
|
||||
}
|
||||
failure {
|
||||
echo 'Build failed!'
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<p align="center">
|
||||
<a href="https://affiliate.postiz.com">
|
||||
<img src="https://github.com/user-attachments/assets/af9f47b3-e20c-402b-bd11-02f39248d738" />
|
||||
<a href="https://github.com/growchief/growchief">
|
||||
<img alt="automate" src="https://github.com/user-attachments/assets/d760188d-8d56-4b05-a6c1-c57e67ef25cd" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- nest start --watch --entryFile=./apps/backend/src/main",
|
||||
"build": "cross-env NODE_ENV=production nest build",
|
||||
"start": "dotenv -e ../../.env -- node ./dist/apps/backend/src/main.js",
|
||||
"start": "dotenv -e ../../.env -- node --experimental-require-module ./dist/apps/backend/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name backend -- start"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments';
|
|||
import { WebhookController } from '@gitroom/backend/api/routes/webhooks.controller';
|
||||
import { SignatureController } from '@gitroom/backend/api/routes/signature.controller';
|
||||
import { AutopostController } from '@gitroom/backend/api/routes/autopost.controller';
|
||||
import { McpService } from '@gitroom/nestjs-libraries/mcp/mcp.service';
|
||||
import { McpController } from '@gitroom/backend/api/routes/mcp.controller';
|
||||
import { SetsController } from '@gitroom/backend/api/routes/sets.controller';
|
||||
import { ThirdPartyController } from '@gitroom/backend/api/routes/third-party.controller';
|
||||
import { MonitorController } from '@gitroom/backend/api/routes/monitor.controller';
|
||||
|
|
@ -63,7 +61,6 @@ const authenticatedController = [
|
|||
StripeController,
|
||||
AuthController,
|
||||
PublicController,
|
||||
McpController,
|
||||
MonitorController,
|
||||
...authenticatedController,
|
||||
],
|
||||
|
|
@ -80,7 +77,6 @@ const authenticatedController = [
|
|||
TrackService,
|
||||
ShortLinkService,
|
||||
Nowpayments,
|
||||
McpService,
|
||||
],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { CreateAgencyDto } from '@gitroom/nestjs-libraries/dtos/agencies/create.
|
|||
export class AgenciesController {
|
||||
constructor(private _agenciesService: AgenciesService) {}
|
||||
@Get('/')
|
||||
async getAgencyByUser(@GetUserFromRequest() user: User) {
|
||||
async getAgencyByUsers(@GetUserFromRequest() user: User) {
|
||||
return (await this._agenciesService.getAgencyByUser(user)) || {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
|||
import { RealIP } from 'nestjs-real-ip';
|
||||
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
|
||||
import { Provider } from '@prisma/client';
|
||||
import * as Sentry from '@sentry/nestjs';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@Controller('/auth')
|
||||
|
|
@ -41,7 +42,7 @@ export class AuthController {
|
|||
async register(
|
||||
@Req() req: Request,
|
||||
@Body() body: CreateOrgUserDto,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@Res({ passthrough: false }) response: Response,
|
||||
@RealIP() ip: string,
|
||||
@UserAgent() userAgent: string
|
||||
) {
|
||||
|
|
@ -101,6 +102,7 @@ export class AuthController {
|
|||
}
|
||||
}
|
||||
|
||||
Sentry.metrics.count("new_user", 1);
|
||||
response.header('onboarding', 'true');
|
||||
response.status(200).json({
|
||||
register: true,
|
||||
|
|
@ -114,7 +116,7 @@ export class AuthController {
|
|||
async login(
|
||||
@Req() req: Request,
|
||||
@Body() body: LoginUserDto,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@Res({ passthrough: false }) response: Response,
|
||||
@RealIP() ip: string,
|
||||
@UserAgent() userAgent: string
|
||||
) {
|
||||
|
|
@ -204,11 +206,11 @@ export class AuthController {
|
|||
@Post('/activate')
|
||||
async activate(
|
||||
@Body('code') code: string,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
@Res({ passthrough: false }) response: Response
|
||||
) {
|
||||
const activate = await this._authService.activate(code);
|
||||
if (!activate) {
|
||||
return response.status(200).send({ can: false });
|
||||
return response.status(200).json({ can: false });
|
||||
}
|
||||
|
||||
response.cookie('auth', activate, {
|
||||
|
|
@ -228,16 +230,18 @@ export class AuthController {
|
|||
}
|
||||
|
||||
response.header('onboarding', 'true');
|
||||
return response.status(200).send({ can: true });
|
||||
|
||||
return response.status(200).json({ can: true });
|
||||
}
|
||||
|
||||
@Post('/oauth/:provider/exists')
|
||||
async oauthExists(
|
||||
@Body('code') code: string,
|
||||
@Param('provider') provider: string,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
@Res({ passthrough: false }) response: Response
|
||||
) {
|
||||
const { jwt, token } = await this._authService.checkExists(provider, code);
|
||||
|
||||
if (token) {
|
||||
return response.json({ token });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,43 @@
|
|||
import { Logger, Controller, Get, Post, Req, Res, Query } from '@nestjs/common';
|
||||
import {
|
||||
Logger,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
Query,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
CopilotRuntime,
|
||||
OpenAIAdapter,
|
||||
copilotRuntimeNestEndpoint,
|
||||
copilotRuntimeNodeHttpEndpoint,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from '@copilotkit/runtime';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { MastraAgent } from '@ag-ui/mastra';
|
||||
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
|
||||
import { Request, Response } from 'express';
|
||||
import { RuntimeContext } from '@mastra/core/di';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
|
||||
export type ChannelsContext = {
|
||||
integrations: string;
|
||||
organization: string;
|
||||
ui: string;
|
||||
};
|
||||
|
||||
@Controller('/copilot')
|
||||
export class CopilotController {
|
||||
constructor(private _subscriptionService: SubscriptionService) {}
|
||||
constructor(
|
||||
private _subscriptionService: SubscriptionService,
|
||||
private _mastraService: MastraService
|
||||
) {}
|
||||
@Post('/chat')
|
||||
chat(@Req() req: Request, @Res() res: Response) {
|
||||
chatAgent(@Req() req: Request, @Res() res: Response) {
|
||||
if (
|
||||
process.env.OPENAI_API_KEY === undefined ||
|
||||
process.env.OPENAI_API_KEY === ''
|
||||
|
|
@ -21,28 +46,112 @@ export class CopilotController {
|
|||
return;
|
||||
}
|
||||
|
||||
const copilotRuntimeHandler = copilotRuntimeNestEndpoint({
|
||||
const copilotRuntimeHandler = copilotRuntimeNodeHttpEndpoint({
|
||||
endpoint: '/copilot/chat',
|
||||
runtime: new CopilotRuntime(),
|
||||
serviceAdapter: new OpenAIAdapter({
|
||||
model:
|
||||
// @ts-ignore
|
||||
req?.body?.variables?.data?.metadata?.requestType ===
|
||||
'TextareaCompletion'
|
||||
? 'gpt-4o-mini'
|
||||
: 'gpt-4.1',
|
||||
model: 'gpt-4.1',
|
||||
}),
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return copilotRuntimeHandler(req, res);
|
||||
}
|
||||
|
||||
@Post('/agent')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
|
||||
async agent(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@GetOrgFromRequest() organization: Organization
|
||||
) {
|
||||
if (
|
||||
process.env.OPENAI_API_KEY === undefined ||
|
||||
process.env.OPENAI_API_KEY === ''
|
||||
) {
|
||||
Logger.warn('OpenAI API key not set, chat functionality will not work');
|
||||
return;
|
||||
}
|
||||
const mastra = await this._mastraService.mastra();
|
||||
const runtimeContext = new RuntimeContext<ChannelsContext>();
|
||||
runtimeContext.set(
|
||||
'integrations',
|
||||
req?.body?.variables?.properties?.integrations || []
|
||||
);
|
||||
|
||||
runtimeContext.set('organization', JSON.stringify(organization));
|
||||
runtimeContext.set('ui', 'true');
|
||||
|
||||
const agents = MastraAgent.getLocalAgents({
|
||||
resourceId: organization.id,
|
||||
mastra,
|
||||
// @ts-ignore
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
agents,
|
||||
});
|
||||
|
||||
const copilotRuntimeHandler = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: '/copilot/agent',
|
||||
runtime,
|
||||
// properties: req.body.variables.properties,
|
||||
serviceAdapter: new OpenAIAdapter({
|
||||
model: 'gpt-4.1',
|
||||
}),
|
||||
});
|
||||
|
||||
return copilotRuntimeHandler.handleRequest(req, res);
|
||||
}
|
||||
|
||||
@Get('/credits')
|
||||
calculateCredits(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Query('type') type: 'ai_images' | 'ai_videos',
|
||||
@Query('type') type: 'ai_images' | 'ai_videos'
|
||||
) {
|
||||
return this._subscriptionService.checkCredits(organization, type || 'ai_images');
|
||||
return this._subscriptionService.checkCredits(
|
||||
organization,
|
||||
type || 'ai_images'
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/:thread/list')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
|
||||
async getMessagesList(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('thread') threadId: string
|
||||
): Promise<any> {
|
||||
const mastra = await this._mastraService.mastra();
|
||||
const memory = await mastra.getAgent('postiz').getMemory();
|
||||
try {
|
||||
return await memory.query({
|
||||
resourceId: organization.id,
|
||||
threadId,
|
||||
});
|
||||
} catch (err) {
|
||||
return { messages: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/list')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
|
||||
async getList(@GetOrgFromRequest() organization: Organization) {
|
||||
const mastra = await this._mastraService.mastra();
|
||||
// @ts-ignore
|
||||
const memory = await mastra.getAgent('postiz').getMemory();
|
||||
const list = await memory.getThreadsByResourceIdPaginated({
|
||||
resourceId: organization.id,
|
||||
perPage: 100000,
|
||||
page: 0,
|
||||
orderBy: 'createdAt',
|
||||
sortDirection: 'DESC',
|
||||
});
|
||||
|
||||
return {
|
||||
threads: list.threads.map((p) => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
|
|
@ -15,7 +16,6 @@ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integ
|
|||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto';
|
||||
import { IntegrationFunctionDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.function.dto';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
||||
|
|
@ -37,6 +37,7 @@ import {
|
|||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
|
|
@ -246,11 +247,68 @@ export class IntegrationsController {
|
|||
) {
|
||||
return this._integrationService.setTimes(org.id, id, body);
|
||||
}
|
||||
|
||||
@Post('/mentions')
|
||||
async mentions(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: IntegrationFunctionDto
|
||||
) {
|
||||
const getIntegration = await this._integrationService.getIntegrationById(
|
||||
org.id,
|
||||
body.id
|
||||
);
|
||||
if (!getIntegration) {
|
||||
throw new Error('Invalid integration');
|
||||
}
|
||||
|
||||
let newList: any[] | { none: true } = [];
|
||||
try {
|
||||
newList = (await this.functionIntegration(org, body)) || [];
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
if (!Array.isArray(newList) && newList?.none) {
|
||||
return newList;
|
||||
}
|
||||
|
||||
const list = await this._integrationService.getMentions(
|
||||
getIntegration.providerIdentifier,
|
||||
body?.data?.query
|
||||
);
|
||||
|
||||
if (Array.isArray(newList) && newList.length) {
|
||||
await this._integrationService.insertMentions(
|
||||
getIntegration.providerIdentifier,
|
||||
newList
|
||||
.map((p: any) => ({
|
||||
name: p.label || '',
|
||||
username: p.id || '',
|
||||
image: p.image || '',
|
||||
doNotCache: p.doNotCache || false,
|
||||
}))
|
||||
.filter((f: any) => f.name && !f.doNotCache)
|
||||
);
|
||||
}
|
||||
|
||||
return uniqBy(
|
||||
[
|
||||
...list.map((p) => ({
|
||||
id: p.username,
|
||||
image: p.image,
|
||||
label: p.name,
|
||||
})),
|
||||
...(newList as any[]),
|
||||
],
|
||||
(p) => p.id
|
||||
).filter((f) => f.label && f.id);
|
||||
}
|
||||
|
||||
@Post('/function')
|
||||
async functionIntegration(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: IntegrationFunctionDto
|
||||
) {
|
||||
): Promise<any> {
|
||||
const getIntegration = await this._integrationService.getIntegrationById(
|
||||
org.id,
|
||||
body.id
|
||||
|
|
@ -266,8 +324,10 @@ export class IntegrationsController {
|
|||
throw new Error('Invalid provider');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (integrationProvider[body.name]) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const load = await integrationProvider[body.name](
|
||||
getIntegration.token,
|
||||
body.data,
|
||||
|
|
@ -427,6 +487,18 @@ export class IntegrationsController {
|
|||
validName = `Channel_${String(id).slice(0, 8)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.STRIPE_PUBLISHABLE_KEY &&
|
||||
org.isTrailing &&
|
||||
(await this._integrationService.checkPreviousConnections(
|
||||
org.id,
|
||||
String(id)
|
||||
))
|
||||
) {
|
||||
throw new HttpException('', 412);
|
||||
}
|
||||
|
||||
return this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Sse,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { McpService } from '@gitroom/nestjs-libraries/mcp/mcp.service';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
|
||||
@ApiTags('Mcp')
|
||||
@Controller('/mcp')
|
||||
export class McpController {
|
||||
constructor(
|
||||
private _mcpService: McpService,
|
||||
private _organizationService: OrganizationService
|
||||
) {}
|
||||
|
||||
@Sse('/:api/sse')
|
||||
async sse(@Param('api') api: string) {
|
||||
const apiModel = await this._organizationService.getOrgByApiKey(api);
|
||||
if (!apiModel) {
|
||||
throw new HttpException('Invalid url', 400);
|
||||
}
|
||||
|
||||
return await this._mcpService.runServer(api, apiModel.id);
|
||||
}
|
||||
|
||||
@Post('/:api/messages')
|
||||
async post(@Param('api') api: string, @Body() body: any) {
|
||||
const apiModel = await this._organizationService.getOrgByApiKey(api);
|
||||
if (!apiModel) {
|
||||
throw new HttpException('Invalid url', 400);
|
||||
}
|
||||
|
||||
return this._mcpService.processPostBody(apiModel.id, body);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,14 @@
|
|||
import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
Res,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
|
|
@ -11,6 +21,10 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service';
|
||||
import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments';
|
||||
import { Readable, pipeline } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const pump = promisify(pipeline);
|
||||
|
||||
@ApiTags('Public')
|
||||
@Controller('/public')
|
||||
|
|
@ -136,4 +150,46 @@ export class PublicController {
|
|||
console.log('cryptoPost', body, path);
|
||||
return this._nowpayments.processPayment(path, body);
|
||||
}
|
||||
|
||||
@Get('/stream')
|
||||
async streamFile(
|
||||
@Query('url') url: string,
|
||||
@Res() res: Response,
|
||||
@Req() req: Request
|
||||
) {
|
||||
if (!url.endsWith('mp4')) {
|
||||
return res.status(400).send('Invalid video URL');
|
||||
}
|
||||
|
||||
const ac = new AbortController();
|
||||
const onClose = () => ac.abort();
|
||||
req.on('aborted', onClose);
|
||||
res.on('close', onClose);
|
||||
|
||||
const r = await fetch(url, { signal: ac.signal });
|
||||
|
||||
if (!r.ok && r.status !== 206) {
|
||||
res.status(r.status);
|
||||
throw new Error(`Upstream error: ${r.statusText}`);
|
||||
}
|
||||
|
||||
const type = r.headers.get('content-type') ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', type);
|
||||
|
||||
const contentRange = r.headers.get('content-range');
|
||||
if (contentRange) res.setHeader('Content-Range', contentRange);
|
||||
|
||||
const len = r.headers.get('content-length');
|
||||
if (len) res.setHeader('Content-Length', len);
|
||||
|
||||
const acceptRanges = r.headers.get('accept-ranges') ?? 'bytes';
|
||||
res.setHeader('Accept-Ranges', acceptRanges);
|
||||
|
||||
if (r.status === 206) res.status(206); // Partial Content for range responses
|
||||
|
||||
try {
|
||||
await pump(Readable.fromWeb(r.body as any), res);
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
|
@ -28,6 +28,14 @@ export class SignatureController {
|
|||
return this._signatureService.createOrUpdateSignature(org.id, body);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
async deleteSignature(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._signatureService.deleteSignature(org.id, id);
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
async updateSignature(
|
||||
@Param('id') id: string,
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ export class UsersController {
|
|||
// Clear Sentry user context on logout
|
||||
clearSentryUserContext();
|
||||
|
||||
response.header('logout', 'true');
|
||||
response.cookie('auth', '', {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
...(!process.env.NOT_SECURED
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import { PublicApiModule } from '@gitroom/backend/public-api/public.api.module';
|
|||
import { ThrottlerBehindProxyGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
|
||||
import { McpModule } from '@gitroom/backend/mcp/mcp.module';
|
||||
import { ThirdPartyModule } from '@gitroom/nestjs-libraries/3rdparties/thirdparty.module';
|
||||
import { VideoModule } from '@gitroom/nestjs-libraries/videos/video.module';
|
||||
import { SentryModule } from "@sentry/nestjs/setup";
|
||||
import { SentryModule } from '@sentry/nestjs/setup';
|
||||
import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
||||
import { ChatModule } from '@gitroom/nestjs-libraries/chat/chat.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
|
@ -23,9 +23,9 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
|||
ApiModule,
|
||||
PublicApiModule,
|
||||
AgentModule,
|
||||
McpModule,
|
||||
ThirdPartyModule,
|
||||
VideoModule,
|
||||
ChatModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
|
|
@ -43,7 +43,7 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
|||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: PoliciesGuard,
|
||||
}
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
BullMqModule,
|
||||
|
|
@ -51,8 +51,8 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
|||
ApiModule,
|
||||
PublicApiModule,
|
||||
AgentModule,
|
||||
McpModule,
|
||||
ThrottlerModule,
|
||||
ChatModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('backend', true);
|
||||
|
||||
import { loadSwagger } from '@gitroom/helpers/swagger/load.swagger';
|
||||
import { json } from 'express';
|
||||
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
|
|
@ -7,37 +11,44 @@ import { Logger, ValidationPipe } from '@nestjs/common';
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('backend', true);
|
||||
|
||||
import { SubscriptionExceptionFilter } from '@gitroom/backend/services/auth/permissions/subscription.exception';
|
||||
import { HttpExceptionFilter } from '@gitroom/nestjs-libraries/services/exception.filter';
|
||||
import { ConfigurationChecker } from '@gitroom/helpers/configuration/configuration.checker';
|
||||
import { startMcp } from '@gitroom/nestjs-libraries/chat/start.mcp';
|
||||
|
||||
async function bootstrap() {
|
||||
async function start() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
rawBody: true,
|
||||
cors: {
|
||||
...(!process.env.NOT_SECURED ? { credentials: true } : {}),
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-copilotkit-runtime-client-gql-version'],
|
||||
exposedHeaders: [
|
||||
'reload',
|
||||
'onboarding',
|
||||
'activate',
|
||||
'x-copilotkit-runtime-client-gql-version',
|
||||
...(process.env.NOT_SECURED ? ['auth', 'showorg', 'impersonate'] : []),
|
||||
],
|
||||
origin: [
|
||||
process.env.FRONTEND_URL,
|
||||
'http://localhost:6274',
|
||||
...(process.env.MAIN_URL ? [process.env.MAIN_URL] : []),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await startMcp(app);
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
})
|
||||
);
|
||||
|
||||
app.use('/copilot/*', (req: any, res: any, next: any) => {
|
||||
json({ limit: '50mb' })(req, res, next);
|
||||
});
|
||||
|
||||
app.use(cookieParser());
|
||||
app.useGlobalFilters(new SubscriptionExceptionFilter());
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
|
@ -69,8 +80,8 @@ function checkConfiguration() {
|
|||
|
||||
Logger.warn('Configuration issues found: ' + checker.getIssuesCount());
|
||||
} else {
|
||||
Logger.log('Configuration check completed without any issues.');
|
||||
Logger.log('Configuration check completed without any issues');
|
||||
}
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
start();
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { McpTool } from '@gitroom/nestjs-libraries/mcp/mcp.tool';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { string, array, enum as eenum, object, boolean } from 'zod';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import dayjs from 'dayjs';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
|
||||
@Injectable()
|
||||
export class MainMcp {
|
||||
constructor(
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService,
|
||||
private _openAiService: OpenaiService
|
||||
) {}
|
||||
|
||||
@McpTool({ toolName: 'POSTIZ_GET_CONFIG_ID' })
|
||||
async preRun() {
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: `id: ${makeId(10)} Today date is ${dayjs.utc().format()}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@McpTool({ toolName: 'POSTIZ_PROVIDERS_LIST' })
|
||||
async listOfProviders(organization: string) {
|
||||
const list = (
|
||||
await this._integrationService.getIntegrationsList(organization)
|
||||
).map((org) => ({
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
identifier: org.providerIdentifier,
|
||||
picture: org.picture,
|
||||
disabled: org.disabled,
|
||||
profile: org.profile,
|
||||
customer: org.customer
|
||||
? {
|
||||
id: org.customer.id,
|
||||
name: org.customer.name,
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return [{ type: 'text', text: JSON.stringify(list) }];
|
||||
}
|
||||
|
||||
@McpTool({
|
||||
toolName: 'POSTIZ_SCHEDULE_POST',
|
||||
zod: {
|
||||
type: eenum(['draft', 'scheduled']),
|
||||
configId: string(),
|
||||
generatePictures: boolean(),
|
||||
date: string().describe('UTC TIME'),
|
||||
providerId: string().describe('Use POSTIZ_PROVIDERS_LIST to get the id'),
|
||||
posts: array(object({ text: string(), images: array(string()) })),
|
||||
},
|
||||
})
|
||||
async schedulePost(
|
||||
organization: string,
|
||||
obj: {
|
||||
type: 'draft' | 'schedule';
|
||||
generatePictures: boolean;
|
||||
date: string;
|
||||
providerId: string;
|
||||
posts: { text: string }[];
|
||||
}
|
||||
) {
|
||||
const create = await this._postsService.createPost(organization, {
|
||||
date: obj.date,
|
||||
type: obj.type,
|
||||
tags: [],
|
||||
shortLink: false,
|
||||
posts: [
|
||||
{
|
||||
group: makeId(10),
|
||||
value: await Promise.all(
|
||||
obj.posts.map(async (post) => ({
|
||||
content: post.text,
|
||||
id: makeId(10),
|
||||
image: !obj.generatePictures
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: makeId(10),
|
||||
path: await this._openAiService.generateImage(
|
||||
post.text,
|
||||
true
|
||||
),
|
||||
},
|
||||
],
|
||||
}))
|
||||
),
|
||||
settings: {
|
||||
__type: 'any' as any,
|
||||
},
|
||||
integration: {
|
||||
id: obj.providerId,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Post created successfully, check it here: ${process.env.FRONTEND_URL}/p/${create[0].postId}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { MainMcp } from '@gitroom/backend/mcp/main.mcp';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [],
|
||||
providers: [MainMcp],
|
||||
get exports() {
|
||||
return [...this.providers];
|
||||
},
|
||||
})
|
||||
export class McpModule {}
|
||||
|
|
@ -34,3 +34,4 @@ export class PublicApiModule implements NestModule {
|
|||
consumer.apply(PublicAuthMiddleware).forRoutes(...authenticatedController);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,12 +26,17 @@ import {
|
|||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { VideoDto } from '@gitroom/nestjs-libraries/dtos/videos/video.dto';
|
||||
import { VideoFunctionDto } from '@gitroom/nestjs-libraries/dtos/videos/video.function.dto';
|
||||
import { UploadDto } from '@gitroom/nestjs-libraries/dtos/media/upload.dto';
|
||||
import axios from 'axios';
|
||||
import { Readable } from 'stream';
|
||||
import { lookup } from 'mime-types';
|
||||
import * as Sentry from '@sentry/nestjs';
|
||||
|
||||
@ApiTags('Public API')
|
||||
@Controller('/public/v1')
|
||||
export class PublicIntegrationsController {
|
||||
private storage = UploadFactory.createStorage();
|
||||
|
||||
|
||||
constructor(
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService,
|
||||
|
|
@ -44,6 +49,7 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@UploadedFile('file') file: Express.Multer.File
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
if (!file) {
|
||||
throw new HttpException({ msg: 'No file provided' }, 400);
|
||||
}
|
||||
|
|
@ -56,11 +62,53 @@ export class PublicIntegrationsController {
|
|||
);
|
||||
}
|
||||
|
||||
@Post('/upload-from-url')
|
||||
async uploadsFromUrl(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: UploadDto
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const response = await axios.get(body.url, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
const buffer = Buffer.from(response.data);
|
||||
|
||||
const getFile = await this.storage.uploadFile({
|
||||
buffer,
|
||||
mimetype: lookup(body?.url?.split?.('?')?.[0]) || 'image/jpeg',
|
||||
size: buffer.length,
|
||||
path: '',
|
||||
fieldname: '',
|
||||
destination: '',
|
||||
stream: new Readable(),
|
||||
filename: '',
|
||||
originalname: '',
|
||||
encoding: '',
|
||||
});
|
||||
|
||||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
getFile.originalname,
|
||||
getFile.path
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/find-slot/:id')
|
||||
async findSlotIntegration(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id?: string
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return { date: await this._postsService.findFreeDateTime(org.id, id) };
|
||||
}
|
||||
|
||||
@Get('/posts')
|
||||
async getPosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetPostsDto
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const posts = await this._postsService.getPosts(org.id, query);
|
||||
return {
|
||||
posts,
|
||||
|
|
@ -74,6 +122,7 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() rawBody: any
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const body = await this._postsService.mapTypeToPost(
|
||||
rawBody,
|
||||
org.id,
|
||||
|
|
@ -90,17 +139,20 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Param() body: { id: string }
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
const getPostById = await this._postsService.getPost(org.id, body.id);
|
||||
return this._postsService.deletePost(org.id, getPostById.group);
|
||||
}
|
||||
|
||||
@Get('/is-connected')
|
||||
async getActiveIntegrations(@GetOrgFromRequest() org: Organization) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return { connected: true };
|
||||
}
|
||||
|
||||
@Get('/integrations')
|
||||
async listIntegration(@GetOrgFromRequest() org: Organization) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return (await this._integrationService.getIntegrationsList(org.id)).map(
|
||||
(org) => ({
|
||||
id: org.id,
|
||||
|
|
@ -124,13 +176,17 @@ export class PublicIntegrationsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: VideoDto
|
||||
) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return this._mediaService.generateVideo(org, body);
|
||||
}
|
||||
|
||||
@Post('/video/function')
|
||||
videoFunction(
|
||||
@Body() body: VideoFunctionDto
|
||||
) {
|
||||
return this._mediaService.videoFunction(body.identifier, body.functionName, body.params);
|
||||
videoFunction(@Body() body: VideoFunctionDto) {
|
||||
Sentry.metrics.count("public_api-request", 1);
|
||||
return this._mediaService.videoFunction(
|
||||
body.identifier,
|
||||
body.functionName,
|
||||
body.params
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/us
|
|||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
|
||||
import { setSentryUserContext } from '@gitroom/nestjs-libraries/sentry/sentry.user.context';
|
||||
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
|
||||
|
||||
export const removeAuth = (res: Response) => {
|
||||
res.cookie('auth', '', {
|
||||
|
|
@ -21,7 +22,6 @@ export const removeAuth = (res: Response) => {
|
|||
expires: new Date(0),
|
||||
maxAge: -1,
|
||||
});
|
||||
|
||||
res.header('logout', 'true');
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o
|
|||
import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service';
|
||||
import { ProvidersFactory } from '@gitroom/backend/services/auth/providers/providers.factory';
|
||||
import dayjs from 'dayjs';
|
||||
import { NewsletterService } from '@gitroom/nestjs-libraries/services/newsletter.service';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto';
|
||||
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
||||
import { NewsletterService } from '@gitroom/nestjs-libraries/newsletter/newsletter.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
|
|
@ -36,10 +36,13 @@ export class AuthService {
|
|||
addToOrg?: boolean | { orgId: string; role: 'USER' | 'ADMIN'; id: string }
|
||||
) {
|
||||
if (provider === Provider.LOCAL) {
|
||||
if (process.env.DISALLOW_PLUS && body.email.includes('+')) {
|
||||
throw new Error('Email with plus sign is not allowed');
|
||||
}
|
||||
const user = await this._userService.getUserByEmail(body.email);
|
||||
if (body instanceof CreateOrgUserDto) {
|
||||
if (user) {
|
||||
throw new Error('User already exists');
|
||||
throw new Error('Email already exists');
|
||||
}
|
||||
|
||||
if (!(await this.canRegister(provider))) {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export class OauthProvider implements ProvidersInterface {
|
|||
redirect_uri: `${this.frontendUrl}/settings`,
|
||||
});
|
||||
|
||||
return `${this.authUrl}/?${params.toString()}`;
|
||||
return `${this.authUrl}?${params.toString()}`;
|
||||
}
|
||||
|
||||
async getToken(code: string): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- nest start --watch --entryFile=./apps/cron/src/main",
|
||||
"build": "cross-env NODE_ENV=production nest build",
|
||||
"start": "dotenv -e ../../.env -- node ./dist/apps/cron/src/main.js",
|
||||
"start": "dotenv -e ../../.env -- node --experimental-require-module ./dist/apps/cron/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name cron -- start"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/databa
|
|||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { SentryModule } from '@sentry/nestjs/setup';
|
||||
import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
||||
import { CheckMissingQueues } from '@gitroom/cron/tasks/check.missing.queues';
|
||||
import { PostNowPendingQueues } from '@gitroom/cron/tasks/post.now.pending.queues';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -13,8 +15,6 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
|||
BullMqModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
FILTER
|
||||
],
|
||||
providers: [FILTER, CheckMissingQueues, PostNowPendingQueues],
|
||||
})
|
||||
export class CronModule {}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { CronModule } from './cron.module';
|
||||
|
||||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('cron');
|
||||
|
||||
async function bootstrap() {
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { CronModule } from './cron.module';
|
||||
|
||||
async function start() {
|
||||
// some comment again
|
||||
await NestFactory.createApplicationContext(CronModule);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
start();
|
||||
|
|
|
|||
45
apps/cron/src/tasks/check.missing.queues.ts
Normal file
45
apps/cron/src/tasks/check.missing.queues.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@Injectable()
|
||||
export class CheckMissingQueues {
|
||||
constructor(
|
||||
private _postService: PostsService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
@Cron('0 * * * *')
|
||||
async handleCron() {
|
||||
const list = await this._postService.searchForMissingThreeHoursPosts();
|
||||
const notExists = (
|
||||
await Promise.all(
|
||||
list.map(async (p) => ({
|
||||
id: p.id,
|
||||
publishDate: p.publishDate,
|
||||
isJob:
|
||||
['delayed', 'waiting'].indexOf(
|
||||
await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)
|
||||
) > -1,
|
||||
}))
|
||||
)
|
||||
).filter((p) => !p.isJob);
|
||||
|
||||
|
||||
for (const job of notExists) {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: job.id,
|
||||
options: {
|
||||
delay: dayjs(job.publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
payload: {
|
||||
id: job.id,
|
||||
delay: dayjs(job.publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
43
apps/cron/src/tasks/post.now.pending.queues.ts
Normal file
43
apps/cron/src/tasks/post.now.pending.queues.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
|
||||
@Injectable()
|
||||
export class PostNowPendingQueues {
|
||||
constructor(
|
||||
private _postService: PostsService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
@Cron('*/16 * * * *')
|
||||
async handleCron() {
|
||||
const list = await this._postService.checkPending15minutesBack();
|
||||
const notExists = (
|
||||
await Promise.all(
|
||||
list.map(async (p) => ({
|
||||
id: p.id,
|
||||
publishDate: p.publishDate,
|
||||
isJob:
|
||||
['delayed', 'waiting'].indexOf(
|
||||
await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)
|
||||
) > -1,
|
||||
}))
|
||||
)
|
||||
).filter((p) => !p.isJob);
|
||||
|
||||
for (const job of notExists) {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: job.id,
|
||||
options: {
|
||||
delay: 0,
|
||||
},
|
||||
payload: {
|
||||
id: job.id,
|
||||
delay: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,31 @@ const nextConfig = {
|
|||
experimental: {
|
||||
proxyTimeout: 90_000,
|
||||
},
|
||||
// Document-Policy header for browser profiling
|
||||
async headers() {
|
||||
return [{
|
||||
source: "/:path*",
|
||||
headers: [{
|
||||
key: "Document-Policy",
|
||||
value: "js-profiling",
|
||||
}, ],
|
||||
}, ];
|
||||
},
|
||||
reactStrictMode: false,
|
||||
transpilePackages: ['crypto-hash'],
|
||||
// Enable production sourcemaps for Sentry
|
||||
productionBrowserSourceMaps: true,
|
||||
|
||||
// Custom webpack config to ensure sourcemaps are generated properly
|
||||
webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
|
||||
// Enable sourcemaps for both client and server in production
|
||||
if (!dev) {
|
||||
config.devtool = isServer ? 'source-map' : 'hidden-source-map';
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
|
@ -42,9 +65,54 @@ const nextConfig = {
|
|||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default withSentryConfig(nextConfig, {
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
|
||||
// Sourcemap configuration optimized for monorepo
|
||||
sourcemaps: {
|
||||
disable: false,
|
||||
// More comprehensive asset patterns for monorepo
|
||||
assets: [
|
||||
".next/static/**/*.js",
|
||||
".next/static/**/*.js.map",
|
||||
".next/server/**/*.js",
|
||||
".next/server/**/*.js.map",
|
||||
],
|
||||
ignore: [
|
||||
"**/node_modules/**",
|
||||
"**/*hot-update*",
|
||||
"**/_buildManifest.js",
|
||||
"**/_ssgManifest.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.js",
|
||||
],
|
||||
deleteSourcemapsAfterUpload: true,
|
||||
},
|
||||
|
||||
// Release configuration
|
||||
release: {
|
||||
create: true,
|
||||
finalize: true,
|
||||
// Use git commit hash for releases in monorepo
|
||||
name: process.env.VERCEL_GIT_COMMIT_SHA || process.env.GITHUB_SHA || undefined,
|
||||
},
|
||||
|
||||
// NextJS specific optimizations for monorepo
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Additional configuration
|
||||
telemetry: false,
|
||||
silent: process.env.NODE_ENV === 'production',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
// Error handling for CI/CD
|
||||
errorHandler: (error) => {
|
||||
console.warn("Sentry build error occurred:", error.message);
|
||||
console.warn("This might be due to missing Sentry environment variables or network issues");
|
||||
// Don't fail the build if Sentry upload fails in monorepo context
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- next dev -p 4200",
|
||||
"build": "next build",
|
||||
"build:sentry": "dotenv -e ../../.env -- next build",
|
||||
"start": "dotenv -e ../../.env -- next start -p 4200",
|
||||
"pm2": "pm2 start pnpm --name frontend -- start"
|
||||
},
|
||||
|
|
|
|||
BIN
apps/frontend/public/icons/platforms/listmonk.png
Normal file
BIN
apps/frontend/public/icons/platforms/listmonk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -10,6 +10,16 @@ import utc from 'dayjs/plugin/utc';
|
|||
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
|
||||
import { CopyClient } from '@gitroom/frontend/components/preview/copy.client';
|
||||
import { getT } from '@gitroom/react/translation/get.translation.service.backend';
|
||||
import dynamicLoad from 'next/dynamic';
|
||||
|
||||
const RenderPreviewDate = dynamicLoad(
|
||||
() =>
|
||||
import('@gitroom/frontend/components/preview/render.preview.date').then(
|
||||
(mod) => mod.RenderPreviewDate
|
||||
),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
dayjs.extend(utc);
|
||||
export const metadata: Metadata = {
|
||||
title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Preview`,
|
||||
|
|
@ -91,11 +101,8 @@ export default async function Auth({
|
|||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
{t('publication_date', 'Publication Date:')}
|
||||
{dayjs
|
||||
.utc(post[0].publishDate)
|
||||
.local()
|
||||
.format('MMMM D, YYYY h:mm A')}
|
||||
{t('publication_date', 'Publication Date:')}{' '}
|
||||
<RenderPreviewDate date={post[0].publishDate} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
12
apps/frontend/src/app/(app)/(site)/agents/[id]/page.tsx
Normal file
12
apps/frontend/src/app/(app)/(site)/agents/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Metadata } from 'next';
|
||||
import { Agent } from '@gitroom/frontend/components/agents/agent';
|
||||
import { AgentChat } from '@gitroom/frontend/components/agents/agent.chat';
|
||||
export const metadata: Metadata = {
|
||||
title: 'Postiz - Agent',
|
||||
description: '',
|
||||
};
|
||||
export default async function Page() {
|
||||
return (
|
||||
<AgentChat />
|
||||
);
|
||||
}
|
||||
13
apps/frontend/src/app/(app)/(site)/agents/layout.tsx
Normal file
13
apps/frontend/src/app/(app)/(site)/agents/layout.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Metadata } from 'next';
|
||||
import { Agent } from '@gitroom/frontend/components/agents/agent';
|
||||
export const metadata: Metadata = {
|
||||
title: 'Postiz - Agent',
|
||||
description: '',
|
||||
};
|
||||
export default async function Layout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <Agent>{children}</Agent>;
|
||||
}
|
||||
11
apps/frontend/src/app/(app)/(site)/agents/page.tsx
Normal file
11
apps/frontend/src/app/(app)/(site)/agents/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Postiz - Agent',
|
||||
description: '',
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
return redirect('/agents/new');
|
||||
}
|
||||
|
|
@ -18,6 +18,13 @@ import { FacebookComponent } from '@gitroom/frontend/components/layout/facebook.
|
|||
import { headers } from 'next/headers';
|
||||
import { headerName } from '@gitroom/react/translation/i18n.config';
|
||||
import { HtmlComponent } from '@gitroom/frontend/components/layout/html.component';
|
||||
// import dynamicLoad from 'next/dynamic';
|
||||
// const SetTimezone = dynamicLoad(
|
||||
// () => import('@gitroom/frontend/components/layout/set.timezone'),
|
||||
// {
|
||||
// ssr: false,
|
||||
// }
|
||||
// );
|
||||
|
||||
const jakartaSans = Plus_Jakarta_Sans({
|
||||
weight: ['600', '500'],
|
||||
|
|
@ -72,6 +79,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
}
|
||||
>
|
||||
<SentryComponent>
|
||||
{/*<SetTimezone />*/}
|
||||
<HtmlComponent />
|
||||
<ToltScript />
|
||||
<FacebookComponent />
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@
|
|||
--new-col-color: #2c2b2b;
|
||||
--new-menu-dots: #696868;
|
||||
--new-menu-hover: #fff;
|
||||
--menu-shadow: 0 8px 30px 0 rgba(0, 0, 0, 0.50);
|
||||
--menu-shadow: 0 8px 30px 0 rgba(0, 0, 0, 0.5);
|
||||
--popup-color: rgba(65, 64, 66, 0.3);
|
||||
}
|
||||
.light {
|
||||
--new-bgColor: #f0f2f4;
|
||||
|
|
@ -33,8 +34,8 @@
|
|||
--new-boxFocused: #ebe8ff;
|
||||
--new-textColor: 14 14 14;
|
||||
--new-blockSeparator: #f2f2f4;
|
||||
--new-btn-simple: #ECEEF1;
|
||||
--new-btn-text: #0E0E0E;
|
||||
--new-btn-simple: #eceef1;
|
||||
--new-btn-text: #0e0e0e;
|
||||
--new-btn-primary: #612bd3;
|
||||
--new-ai-btn: #d82d7e;
|
||||
--new-box-hover: #f4f6f8;
|
||||
|
|
@ -43,15 +44,19 @@
|
|||
--new-table-text: #777b7f;
|
||||
--new-table-text-focused: #fc69ff;
|
||||
--new-small-strips: #191818;
|
||||
--new-big-strips: #F5F7F9;
|
||||
--new-col-color: #EFF1F3;
|
||||
--new-big-strips: #f5f7f9;
|
||||
--new-col-color: #eff1f3;
|
||||
--new-menu-dots: #696868;
|
||||
--new-menu-hover: #000;
|
||||
--menu-shadow: -22px 83px 24px 0 rgba(55, 52, 75, 0.00), -14px 53px 22px 0 rgba(55, 52, 75, 0.01), -8px 30px 19px 0 rgba(55, 52, 75, 0.05), -3px 13px 14px 0 rgba(55, 52, 75, 0.09), -1px 3px 8px 0 rgba(55, 52, 75, 0.10);
|
||||
--menu-shadow: -22px 83px 24px 0 rgba(55, 52, 75, 0),
|
||||
-14px 53px 22px 0 rgba(55, 52, 75, 0.01),
|
||||
-8px 30px 19px 0 rgba(55, 52, 75, 0.05),
|
||||
-3px 13px 14px 0 rgba(55, 52, 75, 0.09),
|
||||
-1px 3px 8px 0 rgba(55, 52, 75, 0.1);
|
||||
--popup-color: rgba(55, 37, 97, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
.dark {
|
||||
--color-primary: #0e0e0e;
|
||||
|
|
@ -193,4 +198,4 @@
|
|||
--color-custom55: #d5d7e1;
|
||||
--color-modalCustom: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -357,6 +357,10 @@ body * {
|
|||
// /*right: -5rem !important;*/
|
||||
//}
|
||||
|
||||
.trz {
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.uppy-FileInput-container {
|
||||
@apply cursor-pointer font-[500] flex justify-center items-center gap-[4px] text-[12px] rounded-[4px] w-[107px] h-[25px] text-textColor border-[2px];
|
||||
@apply bg-customColor3 border-customColor21;
|
||||
|
|
@ -626,3 +630,77 @@ html[dir='rtl'] [dir='ltr'] {
|
|||
.mantine-Overlay-root {
|
||||
background: rgba(65, 64, 66, 0.3) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@apply shadow-menu;
|
||||
background: var(--new-bgColorInner);
|
||||
border: 1px solid var(--new-bgLineColor);
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
button {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
&:hover,
|
||||
&:hover.is-selected {
|
||||
background-color: var(--new-bgLineColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tiptap {
|
||||
a {
|
||||
@apply underline;
|
||||
}
|
||||
}
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mention {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
color: #ae8afc;
|
||||
&::after {
|
||||
content: '\200B';
|
||||
}
|
||||
}
|
||||
}
|
||||
.blur-xs {
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.agent {
|
||||
.copilotKitInputContainer {
|
||||
padding: 0 24px !important;
|
||||
}
|
||||
|
||||
.copilotKitInput {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
.rm-bg .b2 {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.rm-bg .b1 {
|
||||
background: transparent !important;
|
||||
gap: 0 !important;
|
||||
}
|
||||
|
||||
.copilotKitMessage img {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.copilotKitMessage a {
|
||||
color: var(--new-btn-text) !important;
|
||||
}
|
||||
359
apps/frontend/src/components/agents/agent.chat.tsx
Normal file
359
apps/frontend/src/components/agents/agent.chat.tsx
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { CopilotChat, CopilotKitCSSProperties } from '@copilotkit/react-ui';
|
||||
import {
|
||||
InputProps,
|
||||
UserMessageProps,
|
||||
} from '@copilotkit/react-ui/dist/components/chat/props';
|
||||
import { Input } from '@gitroom/frontend/components/agents/agent.input';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import {
|
||||
CopilotKit,
|
||||
useCopilotAction,
|
||||
useCopilotMessagesContext,
|
||||
} from '@copilotkit/react-core';
|
||||
import {
|
||||
MediaPortal,
|
||||
PropertiesContext,
|
||||
} from '@gitroom/frontend/components/agents/agent';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { TextMessage } from '@copilotkit/runtime-client-gql';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
|
||||
import dayjs from 'dayjs';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
|
||||
export const AgentChat: FC = () => {
|
||||
const { backendUrl } = useVariables();
|
||||
const params = useParams<{ id: string }>();
|
||||
const { properties } = useContext(PropertiesContext);
|
||||
|
||||
return (
|
||||
<CopilotKit
|
||||
{...(params.id === 'new' ? {} : { threadId: params.id })}
|
||||
credentials="include"
|
||||
runtimeUrl={backendUrl + '/copilot/agent'}
|
||||
showDevConsole={false}
|
||||
agent="postiz"
|
||||
properties={{
|
||||
integrations: properties,
|
||||
}}
|
||||
>
|
||||
<Hooks />
|
||||
<LoadMessages id={params.id} />
|
||||
<div
|
||||
style={
|
||||
{
|
||||
'--copilot-kit-primary-color': 'var(--new-btn-text)',
|
||||
'--copilot-kit-background-color': 'var(--new-bg-color)',
|
||||
} as CopilotKitCSSProperties
|
||||
}
|
||||
className="trz agent bg-newBgColorInner flex flex-col gap-[15px] transition-all flex-1 items-center relative"
|
||||
>
|
||||
<div className="absolute left-0 w-full h-full pb-[20px]">
|
||||
<CopilotChat
|
||||
className="w-full h-full"
|
||||
labels={{
|
||||
title: 'Your Assistant',
|
||||
initial: `Hello, I am your Postiz agent 🙌🏻.
|
||||
|
||||
I can schedule a post or multiple posts to multiple channels and generate pictures and videos.
|
||||
|
||||
You can select the channels you want to use from the left menu.
|
||||
|
||||
You can see your previous conversations from the right menu.
|
||||
|
||||
You can also use me as an MCP Server, check Settings >> Public API
|
||||
`,
|
||||
}}
|
||||
UserMessage={Message}
|
||||
Input={NewInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CopilotKit>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadMessages: FC<{ id: string }> = ({ id }) => {
|
||||
const { setMessages } = useCopilotMessagesContext();
|
||||
const fetch = useFetch();
|
||||
|
||||
const loadMessages = useCallback(async (idToSet: string) => {
|
||||
const data = await (await fetch(`/copilot/${idToSet}/list`)).json();
|
||||
setMessages(
|
||||
data.uiMessages.map((p: any) => {
|
||||
return new TextMessage({
|
||||
content: p.content,
|
||||
role: p.role,
|
||||
});
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (id === 'new') {
|
||||
setMessages([]);
|
||||
return;
|
||||
}
|
||||
loadMessages(id);
|
||||
}, [id]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const Message: FC<UserMessageProps> = (props) => {
|
||||
const convertContentToImagesAndVideo = useMemo(() => {
|
||||
return (props.message?.content || '')
|
||||
.replace(/Video: (http.*mp4\n)/g, (match, p1) => {
|
||||
return `<video controls class="h-[150px] w-[150px] rounded-[8px] mb-[10px]"><source src="${p1.trim()}" type="video/mp4">Your browser does not support the video tag.</video>`;
|
||||
})
|
||||
.replace(/Image: (http.*\n)/g, (match, p1) => {
|
||||
return `<img src="${p1.trim()}" class="h-[150px] w-[150px] max-w-full border border-newBgColorInner" />`;
|
||||
})
|
||||
.replace(/\[\-\-Media\-\-\](.*)\[\-\-Media\-\-\]/g, (match, p1) => {
|
||||
return `<div class="flex justify-center mt-[20px]">${p1}</div>`;
|
||||
})
|
||||
.replace(
|
||||
/(\[--integrations--\][\s\S]*?\[--integrations--\])/g,
|
||||
(match, p1) => {
|
||||
return ``;
|
||||
}
|
||||
);
|
||||
}, [props.message?.content]);
|
||||
return (
|
||||
<div
|
||||
className="copilotKitMessage copilotKitUserMessage min-w-[300px]"
|
||||
dangerouslySetInnerHTML={{ __html: convertContentToImagesAndVideo }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const NewInput: FC<InputProps> = (props) => {
|
||||
const [media, setMedia] = useState([] as { path: string; id: string }[]);
|
||||
const [value, setValue] = useState('');
|
||||
const { properties } = useContext(PropertiesContext);
|
||||
return (
|
||||
<>
|
||||
<MediaPortal
|
||||
value={value}
|
||||
media={media}
|
||||
setMedia={(e) => setMedia(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
{...props}
|
||||
onChange={setValue}
|
||||
onSend={(text) => {
|
||||
const send = props.onSend(
|
||||
text +
|
||||
(media.length > 0
|
||||
? '\n[--Media--]' +
|
||||
media
|
||||
.map((m) =>
|
||||
m.path.indexOf('mp4') > -1
|
||||
? `Video: ${m.path}`
|
||||
: `Image: ${m.path}`
|
||||
)
|
||||
.join('\n') +
|
||||
'\n[--Media--]'
|
||||
: '') +
|
||||
`
|
||||
${
|
||||
properties.length
|
||||
? `[--integrations--]
|
||||
Use the following social media platforms: ${JSON.stringify(
|
||||
properties.map((p) => ({
|
||||
id: p.id,
|
||||
platform: p.identifier,
|
||||
profilePicture: p.picture,
|
||||
additionalSettings: p.additionalSettings,
|
||||
}))
|
||||
)}
|
||||
[--integrations--]`
|
||||
: ``
|
||||
}`
|
||||
);
|
||||
setValue('');
|
||||
setMedia([]);
|
||||
return send;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Hooks: FC = () => {
|
||||
const modals = useModals();
|
||||
|
||||
useCopilotAction({
|
||||
name: 'manualPosting',
|
||||
description:
|
||||
'This tool should be triggered when the user wants to manually add the generated post',
|
||||
parameters: [
|
||||
{
|
||||
name: 'list',
|
||||
type: 'object[]',
|
||||
description:
|
||||
'list of posts to schedule to different social media (integration ids)',
|
||||
attributes: [
|
||||
{
|
||||
name: 'integrationId',
|
||||
type: 'string',
|
||||
description: 'The integration id',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
type: 'string',
|
||||
description: 'UTC date of the scheduled post',
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
type: 'object',
|
||||
description: 'Settings for the integration [input:settings]',
|
||||
},
|
||||
{
|
||||
name: 'posts',
|
||||
type: 'object[]',
|
||||
description: 'list of posts / comments (one under another)',
|
||||
attributes: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
description: 'the content of the post',
|
||||
},
|
||||
{
|
||||
name: 'attachments',
|
||||
type: 'object[]',
|
||||
description: 'list of attachments',
|
||||
attributes: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
description: 'id of the attachment',
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
description: 'url of the attachment',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
renderAndWaitForResponse: ({ args, status, respond }) => {
|
||||
if (status === 'executing') {
|
||||
return <OpenModal args={args} respond={respond} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
const OpenModal: FC<{
|
||||
respond: (value: any) => void;
|
||||
args: {
|
||||
list: {
|
||||
integrationId: string;
|
||||
date: string;
|
||||
settings?: Record<string, any>;
|
||||
posts: { content: string; attachments: { id: string; path: string }[] }[];
|
||||
}[];
|
||||
};
|
||||
}> = ({ args, respond }) => {
|
||||
const modals = useModals();
|
||||
const { properties } = useContext(PropertiesContext);
|
||||
const startModal = useCallback(async () => {
|
||||
for (const integration of args.list) {
|
||||
await new Promise((res) => {
|
||||
const group = makeId(10);
|
||||
modals.openModal({
|
||||
id: 'add-edit-modal',
|
||||
closeOnClickOutside: false,
|
||||
removeLayout: true,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
askClose: true,
|
||||
size: '80%',
|
||||
title: ``,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] text-textColor',
|
||||
},
|
||||
children: (
|
||||
<ExistingDataContextProvider
|
||||
value={{
|
||||
group,
|
||||
integration: integration.integrationId,
|
||||
integrationPicture:
|
||||
properties.find((p) => p.id === integration.integrationId)
|
||||
.picture || '',
|
||||
settings: integration.settings || {},
|
||||
posts: integration.posts.map((p) => ({
|
||||
approvedSubmitForOrder: 'NO',
|
||||
content: p.content,
|
||||
createdAt: new Date().toISOString(),
|
||||
state: 'DRAFT',
|
||||
id: makeId(10),
|
||||
settings: JSON.stringify(integration.settings || {}),
|
||||
group,
|
||||
integrationId: integration.integrationId,
|
||||
integration: properties.find(
|
||||
(p) => p.id === integration.integrationId
|
||||
),
|
||||
publishDate: dayjs.utc(integration.date).toISOString(),
|
||||
image: p.attachments.map((a) => ({
|
||||
id: a.id,
|
||||
path: a.path,
|
||||
})),
|
||||
})),
|
||||
}}
|
||||
>
|
||||
<AddEditModal
|
||||
date={dayjs.utc(integration.date)}
|
||||
allIntegrations={properties}
|
||||
integrations={properties.filter(
|
||||
(p) => p.id === integration.integrationId
|
||||
)}
|
||||
onlyValues={integration.posts.map((p) => ({
|
||||
content: p.content,
|
||||
id: makeId(10),
|
||||
settings: integration.settings || {},
|
||||
image: p.attachments.map((a) => ({
|
||||
id: a.id,
|
||||
path: a.path,
|
||||
})),
|
||||
}))}
|
||||
reopenModal={() => {}}
|
||||
mutate={() => res(true)}
|
||||
/>
|
||||
</ExistingDataContextProvider>
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
respond('User scheduled all the posts');
|
||||
}, [args, respond, properties]);
|
||||
|
||||
useEffect(() => {
|
||||
startModal();
|
||||
}, []);
|
||||
return (
|
||||
<div onClick={() => respond('continue')}>
|
||||
Opening manually ${JSON.stringify(args)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
120
apps/frontend/src/components/agents/agent.input.tsx
Normal file
120
apps/frontend/src/components/agents/agent.input.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { useCopilotContext, useCopilotReadable } from '@copilotkit/react-core';
|
||||
import AutoResizingTextarea from '@gitroom/frontend/components/agents/agent.textarea';
|
||||
import { useChatContext } from '@copilotkit/react-ui';
|
||||
import { InputProps } from '@copilotkit/react-ui/dist/components/chat/props';
|
||||
const MAX_NEWLINES = 6;
|
||||
|
||||
export const Input = ({
|
||||
inProgress,
|
||||
onSend,
|
||||
isVisible = false,
|
||||
onStop,
|
||||
onUpload,
|
||||
hideStopButton = false,
|
||||
onChange,
|
||||
}: InputProps & { onChange: (value: string) => void }) => {
|
||||
const context = useChatContext();
|
||||
const copilotContext = useCopilotContext();
|
||||
const showPoweredBy = !copilotContext.copilotApiConfig?.publicApiKey;
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
|
||||
const handleDivClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// If the user clicked a button or inside a button, don't focus the textarea
|
||||
if (target.closest('button')) return;
|
||||
|
||||
// If the user clicked the textarea, do nothing (it's already focused)
|
||||
if (target.tagName === 'TEXTAREA') return;
|
||||
|
||||
// Otherwise, focus the textarea
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
const [text, setText] = useState('');
|
||||
const send = () => {
|
||||
if (inProgress) return;
|
||||
onSend(text);
|
||||
setText('');
|
||||
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
const isInProgress = inProgress;
|
||||
const buttonIcon =
|
||||
isInProgress && !hideStopButton
|
||||
? context.icons.stopIcon
|
||||
: context.icons.sendIcon;
|
||||
|
||||
const canSend = useMemo(() => {
|
||||
const interruptEvent = copilotContext.langGraphInterruptAction?.event;
|
||||
const interruptInProgress =
|
||||
interruptEvent?.name === 'LangGraphInterruptEvent' &&
|
||||
!interruptEvent?.response;
|
||||
|
||||
return !isInProgress && text.trim().length > 0 && !interruptInProgress;
|
||||
}, [copilotContext.langGraphInterruptAction?.event, isInProgress, text]);
|
||||
|
||||
const canStop = useMemo(() => {
|
||||
return isInProgress && !hideStopButton;
|
||||
}, [isInProgress, hideStopButton]);
|
||||
|
||||
const sendDisabled = !canSend && !canStop;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`copilotKitInputContainer ${
|
||||
showPoweredBy ? 'poweredByContainer' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="copilotKitInput" onClick={handleDivClick}>
|
||||
<AutoResizingTextarea
|
||||
ref={textareaRef}
|
||||
placeholder={context.labels.placeholder}
|
||||
autoFocus={false}
|
||||
maxRows={MAX_NEWLINES}
|
||||
value={text}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
setText(event.target.value);
|
||||
}}
|
||||
onCompositionStart={() => setIsComposing(true)}
|
||||
onCompositionEnd={() => setIsComposing(false)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey && !isComposing) {
|
||||
event.preventDefault();
|
||||
if (canSend) {
|
||||
send();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="copilotKitInputControls">
|
||||
{onUpload && (
|
||||
<button onClick={onUpload} className="copilotKitInputControlButton">
|
||||
{context.icons.uploadIcon}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
<button
|
||||
disabled={sendDisabled}
|
||||
onClick={isInProgress && !hideStopButton ? onStop : send}
|
||||
data-copilotkit-in-progress={inProgress}
|
||||
data-test-id={
|
||||
inProgress
|
||||
? 'copilot-chat-request-in-progress'
|
||||
: 'copilot-chat-ready'
|
||||
}
|
||||
className="copilotKitInputControlButton"
|
||||
>
|
||||
{buttonIcon}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
77
apps/frontend/src/components/agents/agent.textarea.tsx
Normal file
77
apps/frontend/src/components/agents/agent.textarea.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
|
||||
|
||||
interface AutoResizingTextareaProps {
|
||||
maxRows?: number;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onCompositionStart?: () => void;
|
||||
onCompositionEnd?: () => void;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const AutoResizingTextarea = forwardRef<HTMLTextAreaElement, AutoResizingTextareaProps>(
|
||||
(
|
||||
{
|
||||
maxRows = 1,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onCompositionStart,
|
||||
onCompositionEnd,
|
||||
autoFocus,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const internalTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [maxHeight, setMaxHeight] = useState<number>(0);
|
||||
|
||||
useImperativeHandle(ref, () => internalTextareaRef.current as HTMLTextAreaElement);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateMaxHeight = () => {
|
||||
const textarea = internalTextareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto";
|
||||
const singleRowHeight = textarea.scrollHeight;
|
||||
setMaxHeight(singleRowHeight * maxRows);
|
||||
if (autoFocus) {
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
calculateMaxHeight();
|
||||
}, [maxRows]);
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = internalTextareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto";
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
|
||||
}
|
||||
}, [value, maxHeight]);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={internalTextareaRef}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onCompositionStart={onCompositionStart}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
overflow: "auto",
|
||||
resize: "none",
|
||||
maxHeight: `${maxHeight}px`,
|
||||
}}
|
||||
rows={1}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default AutoResizingTextarea;
|
||||
271
apps/frontend/src/components/agents/agent.tsx
Normal file
271
apps/frontend/src/components/agents/agent.tsx
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useCookie from 'react-use-cookie';
|
||||
import useSWR from 'swr';
|
||||
import { orderBy } from 'lodash';
|
||||
import { SVGLine } from '@gitroom/frontend/components/launches/launches.component';
|
||||
import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
|
||||
import Image from 'next/image';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useWaitForClass } from '@gitroom/helpers/utils/use.wait.for.class';
|
||||
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { Integration } from '@prisma/client';
|
||||
import Link from 'next/link';
|
||||
import { useParams, usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
export const MediaPortal: FC<{
|
||||
media: { path: string; id: string }[];
|
||||
value: string;
|
||||
setMedia: (event: {
|
||||
target: {
|
||||
name: string;
|
||||
value?: {
|
||||
id: string;
|
||||
path: string;
|
||||
alt?: string;
|
||||
thumbnail?: string;
|
||||
thumbnailTimestamp?: number;
|
||||
}[];
|
||||
};
|
||||
}) => void;
|
||||
}> = ({ media, setMedia, value }) => {
|
||||
const waitForClass = useWaitForClass('copilotKitMessages');
|
||||
if (!waitForClass) return null;
|
||||
return (
|
||||
<div className="pl-[14px] pr-[24px] whitespace-nowrap editor rm-bg">
|
||||
<MultiMediaComponent
|
||||
allData={[{ content: value }]}
|
||||
text={value}
|
||||
label="Attachments"
|
||||
description=""
|
||||
value={media}
|
||||
dummy={false}
|
||||
name="image"
|
||||
onChange={setMedia}
|
||||
onOpen={() => {}}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentList: FC<{ onChange: (arr: any[]) => void }> = ({
|
||||
onChange,
|
||||
}) => {
|
||||
const fetch = useFetch();
|
||||
const [selected, setSelected] = useState([]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
return (await (await fetch('/integrations/list')).json()).integrations;
|
||||
}, []);
|
||||
|
||||
const [collapseMenu, setCollapseMenu] = useCookie('collapseMenu', '0');
|
||||
|
||||
const { data } = useSWR('integrations', load, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
fallbackData: [],
|
||||
});
|
||||
|
||||
const setIntegration = useCallback(
|
||||
(integration: Integration) => () => {
|
||||
if (selected.some((p) => p.id === integration.id)) {
|
||||
onChange(selected.filter((p) => p.id !== integration.id));
|
||||
setSelected(selected.filter((p) => p.id !== integration.id));
|
||||
} else {
|
||||
onChange([...selected, integration]);
|
||||
setSelected([...selected, integration]);
|
||||
}
|
||||
},
|
||||
[selected]
|
||||
);
|
||||
|
||||
const sortedIntegrations = useMemo(() => {
|
||||
return orderBy(
|
||||
data || [],
|
||||
['type', 'disabled', 'identifier'],
|
||||
['desc', 'asc', 'asc']
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'trz bg-newBgColorInner flex flex-col gap-[15px] transition-all relative',
|
||||
collapseMenu === '1' ? 'group sidebar w-[100px]' : 'w-[260px]'
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 start-0 w-full h-full p-[20px] overflow-auto scrollbar scrollbar-thumb-fifth scrollbar-track-newBgColor">
|
||||
<div className="flex items-center">
|
||||
<h2 className="group-[.sidebar]:hidden flex-1 text-[20px] font-[500] mb-[15px]">
|
||||
Select Channels
|
||||
</h2>
|
||||
<div
|
||||
onClick={() => setCollapseMenu(collapseMenu === '1' ? '0' : '1')}
|
||||
className="-mt-3 group-[.sidebar]:rotate-[180deg] group-[.sidebar]:mx-auto text-btnText bg-btnSimple rounded-[6px] w-[24px] h-[24px] flex items-center justify-center cursor-pointer select-none"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="7"
|
||||
height="13"
|
||||
viewBox="0 0 7 13"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M6 11.5L1 6.5L6 1.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx('flex flex-col gap-[15px]')}>
|
||||
{sortedIntegrations.map((integration, index) => (
|
||||
<div
|
||||
onClick={setIntegration(integration)}
|
||||
key={integration.id}
|
||||
className={clsx(
|
||||
'flex gap-[12px] items-center group/profile justify-center hover:bg-boxHover rounded-e-[8px] hover:opacity-100 cursor-pointer',
|
||||
!selected.some((p) => p.id === integration.id) && 'opacity-20'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative rounded-full flex justify-center items-center gap-[6px]',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{(integration.inBetweenSteps || integration.refreshNeeded) && (
|
||||
<div className="absolute start-0 top-0 w-[39px] h-[46px] cursor-pointer">
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full start-0 -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-primary/60 w-[39px] h-[46px] start-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full w-[4px] -ms-[12px] rounded-s-[3px] opacity-0 group-hover/profile:opacity-100 transition-opacity">
|
||||
<SVGLine />
|
||||
</div>
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture}
|
||||
className="rounded-[8px]"
|
||||
alt={integration.identifier}
|
||||
width={36}
|
||||
height={36}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-[8px] absolute z-10 bottom-[5px] -end-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={18.41}
|
||||
height={18.41}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden group-[.sidebar]:hidden',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PropertiesContext = createContext({ properties: [] });
|
||||
export const Agent: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [properties, setProperties] = useState([]);
|
||||
|
||||
return (
|
||||
<PropertiesContext.Provider value={{ properties }}>
|
||||
<AgentList onChange={setProperties} />
|
||||
<div className="bg-newBgColorInner flex flex-1">{children}</div>
|
||||
<Threads />
|
||||
</PropertiesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const Threads: FC = () => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const threads = useCallback(async () => {
|
||||
return (await fetch('/copilot/list')).json();
|
||||
}, []);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const { data } = useSWR('threads', threads);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'trz bg-newBgColorInner flex flex-col gap-[15px] transition-all relative',
|
||||
'w-[260px]'
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 start-0 w-full h-full p-[20px] overflow-auto scrollbar scrollbar-thumb-fifth scrollbar-track-newBgColor">
|
||||
<div className="mb-[15px] justify-center flex group-[.sidebar]:pb-[15px]">
|
||||
<Link
|
||||
href={`/agents`}
|
||||
className="text-white whitespace-nowrap flex-1 pt-[12px] pb-[14px] ps-[16px] pe-[20px] group-[.sidebar]:p-0 min-h-[44px] max-h-[44px] rounded-md bg-btnPrimary flex justify-center items-center gap-[5px] outline-none"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
className="min-w-[21px] min-h-[20px]"
|
||||
>
|
||||
<path
|
||||
d="M10.5001 4.16699V15.8337M4.66675 10.0003H16.3334"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex-1 text-start text-[16px] group-[.sidebar]:hidden">
|
||||
Start a new chat
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[1px]">
|
||||
{data?.threads?.map((p: any) => (
|
||||
<Link
|
||||
className={clsx(
|
||||
'overflow-ellipsis overflow-hidden whitespace-nowrap hover:bg-newBgColor px-[10px] py-[6px] rounded-[10px] cursor-pointer',
|
||||
p.id === id && 'bg-newBgColor'
|
||||
)}
|
||||
href={`/agents/${p.id}`}
|
||||
key={p.id}
|
||||
>
|
||||
{p.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
StarsList,
|
||||
} from '@gitroom/frontend/components/analytics/stars.and.forks.interface';
|
||||
import dayjs from 'dayjs';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
export const Chart: FC<{
|
||||
list: StarsList[] | ForksList[];
|
||||
}> = (props) => {
|
||||
|
|
@ -48,7 +49,7 @@ export const Chart: FC<{
|
|||
},
|
||||
},
|
||||
data: {
|
||||
labels: list.map((row) => dayjs(row.date).format('DD/MM/YYYY')),
|
||||
labels: list.map((row) => newDayjs(row.date).format('DD/MM/YYYY')),
|
||||
datasets: [
|
||||
{
|
||||
borderColor: '#fff',
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export function RegisterAfter({
|
|||
...data,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
setLoading(false);
|
||||
if (response.status === 200) {
|
||||
fireEvents('register');
|
||||
|
|
@ -129,7 +129,7 @@ export function RegisterAfter({
|
|||
});
|
||||
} else {
|
||||
form.setError('email', {
|
||||
message: getHelpfulReasonForRegistrationFailure(response.status),
|
||||
message: await response.text(),
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { FC, Fragment, useCallback, useMemo, useState } from 'react';
|
|||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
|
@ -28,11 +28,8 @@ export const Autopost: FC = () => {
|
|||
const addWebhook = useCallback(
|
||||
(data?: any) => () => {
|
||||
modal.openModal({
|
||||
title: '',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
title: data ? 'Edit Autopost' : 'Add Autopost',
|
||||
withCloseButton: true,
|
||||
children: <AddOrEditWebhook data={data} reload={mutate} />,
|
||||
});
|
||||
},
|
||||
|
|
@ -228,7 +225,14 @@ export const AddOrEditWebhook: FC<{
|
|||
},
|
||||
[]
|
||||
);
|
||||
const { data: dataList, isLoading } = useSWR('integrations', integration);
|
||||
const { data: dataList, isLoading } = useSWR('integrations', integration, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
});
|
||||
const callBack = useCallback(
|
||||
async (values: any) => {
|
||||
await fetch(data?.id ? `/autopost/${data?.id}` : '/autopost', {
|
||||
|
|
@ -289,29 +293,7 @@ export const AddOrEditWebhook: FC<{
|
|||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(callBack)}>
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 bg-sixth p-[16px] pt-0 w-[500px]">
|
||||
<TopTitle title={data ? 'Edit autopost' : 'Add autopost'} />
|
||||
<button
|
||||
className="outline-none absolute end-[20px] top-[15px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
onClick={modal.closeAll}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 pt-0">
|
||||
<div>
|
||||
<Input
|
||||
label="Title"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { useSWRConfig } from 'swr';
|
|||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
||||
|
|
@ -28,6 +28,7 @@ import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
|||
import { PurchaseCrypto } from '@gitroom/frontend/components/billing/purchase.crypto';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { FinishTrial } from '@gitroom/frontend/components/billing/finish.trial';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
|
||||
export const Prorate: FC<{
|
||||
period: 'MONTHLY' | 'YEARLY';
|
||||
|
|
@ -376,7 +377,7 @@ export const MainBillingComponent: FC<{
|
|||
[monthlyOrYearly, subscription, user, utm]
|
||||
);
|
||||
if (user?.isLifetime) {
|
||||
router.replace('/billing/lifetime');
|
||||
router.replace('/');
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
|
@ -504,8 +505,8 @@ export const MainBillingComponent: FC<{
|
|||
{t(
|
||||
'your_subscription_will_be_canceled_at',
|
||||
'Your subscription will be canceled at'
|
||||
)}
|
||||
{dayjs(subscription.cancelAt).local().format('D MMM, YYYY')}
|
||||
)}{' '}
|
||||
{newDayjs(subscription.cancelAt).local().format('D MMM, YYYY')}
|
||||
<br />
|
||||
{t(
|
||||
'you_will_never_be_charged_again',
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import { useModals } from '@mantine/modals';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import React, { FC, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
|
@ -24,17 +24,9 @@ export const useAddProvider = (update?: () => void) => {
|
|||
return useCallback(async () => {
|
||||
const data = await (await fetch('/integrations')).json();
|
||||
modal.openModal({
|
||||
title: '',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'text-textColor',
|
||||
},
|
||||
size: 'auto',
|
||||
children: (
|
||||
<ModalWrapperComponent title="Add Channel">
|
||||
<AddProviderComponent update={update} {...data} />
|
||||
</ModalWrapperComponent>
|
||||
),
|
||||
title: 'Add Channel',
|
||||
withCloseButton: true,
|
||||
children: <AddProviderComponent update={update} {...data} />,
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
|
@ -42,9 +34,16 @@ export const AddProviderButton: FC<{
|
|||
update?: () => void;
|
||||
}> = (props) => {
|
||||
const { update } = props;
|
||||
const query = useSearchParams();
|
||||
const add = useAddProvider(update);
|
||||
const t = useT();
|
||||
|
||||
useEffect(() => {
|
||||
if (query.get('onboarding')) {
|
||||
add();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
className="text-btnText bg-btnSimple h-[44px] pt-[12px] pb-[14px] ps-[16px] pe-[20px] justify-center items-center flex rounded-[8px] gap-[8px]"
|
||||
|
|
@ -259,6 +258,7 @@ export const CustomVariables: FC<{
|
|||
});
|
||||
const submit = useCallback(
|
||||
async (data: FieldValues) => {
|
||||
modals.closeAll();
|
||||
gotoUrl(
|
||||
`/integrations/social/${identifier}?state=nostate&code=${Buffer.from(
|
||||
JSON.stringify(data)
|
||||
|
|
@ -342,7 +342,7 @@ export const AddProviderComponent: FC<{
|
|||
await fetch(`/integrations/social/${identifier}`)
|
||||
).json();
|
||||
modal.openModal({
|
||||
title: '',
|
||||
title: 'Web3 provider',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
|
|
@ -379,7 +379,7 @@ export const AddProviderComponent: FC<{
|
|||
if (isExternal) {
|
||||
modal.closeAll();
|
||||
modal.openModal({
|
||||
title: '',
|
||||
title: 'URL',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
|
|
@ -391,7 +391,7 @@ export const AddProviderComponent: FC<{
|
|||
if (customFields) {
|
||||
modal.closeAll();
|
||||
modal.openModal({
|
||||
title: '',
|
||||
title: 'Add Provider',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
|
|
@ -410,9 +410,7 @@ export const AddProviderComponent: FC<{
|
|||
},
|
||||
[]
|
||||
);
|
||||
const close = useCallback(() => {
|
||||
modal.closeAll();
|
||||
}, []);
|
||||
|
||||
const showApiButton = useCallback(
|
||||
(identifier: string, name: string) => async () => {
|
||||
modal.openModal({
|
||||
|
|
@ -434,7 +432,6 @@ export const AddProviderComponent: FC<{
|
|||
return (
|
||||
<div className="w-full flex flex-col gap-[20px] rounded-[4px] relative">
|
||||
<div className="flex flex-col">
|
||||
<h2 className="pt-[16px] pb-[10px]">{t('social', 'Social')}</h2>
|
||||
<div className="grid grid-cols-5 gap-[10px] justify-items-center justify-center">
|
||||
{social.map((item) => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import React, { FC, FormEventHandler, useCallback, useState } from 'react';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
|
|
|||
|
|
@ -20,12 +20,13 @@ import isoWeek from 'dayjs/plugin/isoWeek';
|
|||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import { extend } from 'dayjs';
|
||||
import useCookie from 'react-use-cookie';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
extend(isoWeek);
|
||||
extend(weekOfYear);
|
||||
|
||||
export const CalendarContext = createContext({
|
||||
startDate: dayjs().startOf('isoWeek').format('YYYY-MM-DD'),
|
||||
endDate: dayjs().endOf('isoWeek').format('YYYY-MM-DD'),
|
||||
startDate: newDayjs().startOf('isoWeek').format('YYYY-MM-DD'),
|
||||
endDate: newDayjs().endOf('isoWeek').format('YYYY-MM-DD'),
|
||||
customer: null as string | null,
|
||||
sets: [] as { name: string; id: string; content: string[] }[],
|
||||
signature: undefined as any,
|
||||
|
|
@ -86,7 +87,7 @@ export interface Integrations {
|
|||
|
||||
// Helper function to get start and end dates based on display type
|
||||
function getDateRange(display: string, referenceDate?: string) {
|
||||
const date = referenceDate ? dayjs(referenceDate) : dayjs();
|
||||
const date = referenceDate ? newDayjs(referenceDate) : newDayjs();
|
||||
|
||||
switch (display) {
|
||||
case 'day':
|
||||
|
|
@ -153,8 +154,8 @@ export const CalendarWeekProvider: FC<{
|
|||
const modifiedParams = new URLSearchParams({
|
||||
display: filters.display,
|
||||
customer: filters?.customer?.toString() || '',
|
||||
startDate: dayjs(filters.startDate).startOf('day').utc().format(),
|
||||
endDate: dayjs(filters.endDate).endOf('day').utc().format(),
|
||||
startDate: newDayjs(filters.startDate).startOf('day').utc().format(),
|
||||
endDate: newDayjs(filters.endDate).endOf('day').utc().format(),
|
||||
}).toString();
|
||||
|
||||
const data = (await fetch(`/posts?${modifiedParams}`)).json();
|
||||
|
|
@ -176,8 +177,22 @@ export const CalendarWeekProvider: FC<{
|
|||
return (await fetch('/sets')).json();
|
||||
}, []);
|
||||
|
||||
const { data: sets, mutate } = useSWR('sets', setList);
|
||||
const { data: sign } = useSWR('default-sign', defaultSign);
|
||||
const { data: sets, mutate } = useSWR('sets', setList, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
});
|
||||
const { data: sign } = useSWR('default-sign', defaultSign, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
});
|
||||
|
||||
const setFiltersWrapper = useCallback(
|
||||
(filters: {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import 'dayjs/locale/ar';
|
|||
import 'dayjs/locale/tr';
|
||||
import 'dayjs/locale/vi';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import clsx from 'clsx';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
|
|
@ -53,7 +53,7 @@ import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.m
|
|||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { ModalWrapperComponent } from '../new-launch/modal.wrapper.component';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
|
||||
// Extend dayjs with necessary plugins
|
||||
extend(isSameOrAfter);
|
||||
|
|
@ -146,7 +146,7 @@ export const DayView = () => {
|
|||
{options.map((option) => (
|
||||
<Fragment key={option[0].time}>
|
||||
<div className="text-center text-[14px]">
|
||||
{dayjs()
|
||||
{newDayjs()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.add(option[0].time, 'minute')
|
||||
|
|
@ -186,7 +186,7 @@ export const WeekView = () => {
|
|||
dayjs.locale(currentLanguage);
|
||||
|
||||
const days = [];
|
||||
const weekStart = dayjs(startDate);
|
||||
const weekStart = newDayjs(startDate);
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = weekStart.add(i, 'day');
|
||||
days.push({
|
||||
|
|
@ -214,10 +214,11 @@ export const WeekView = () => {
|
|||
<div
|
||||
className={clsx(
|
||||
'text-[14px] font-[600] flex items-center justify-center gap-[6px]',
|
||||
day.day === dayjs().format('L') && 'text-newTableTextFocused'
|
||||
day.day === newDayjs().format('L') &&
|
||||
'text-newTableTextFocused'
|
||||
)}
|
||||
>
|
||||
{day.day === dayjs().format('L') && (
|
||||
{day.day === newDayjs().format('L') && (
|
||||
<div className="w-[6px] h-[6px] bg-newTableTextFocused rounded-full" />
|
||||
)}
|
||||
{day.day}
|
||||
|
|
@ -259,17 +260,17 @@ export const MonthView = () => {
|
|||
const days = [];
|
||||
// Starting from Monday (1) to Sunday (7)
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
days.push(dayjs().day(i).format('dddd'));
|
||||
days.push(newDayjs().day(i).format('dddd'));
|
||||
}
|
||||
return days;
|
||||
}, [i18next.resolvedLanguage]);
|
||||
|
||||
const calendarDays = useMemo(() => {
|
||||
const monthStart = dayjs(startDate);
|
||||
const monthStart = newDayjs(startDate);
|
||||
const currentMonth = monthStart.month();
|
||||
const currentYear = monthStart.year();
|
||||
|
||||
const startOfMonth = dayjs(new Date(currentYear, currentMonth, 1));
|
||||
const startOfMonth = newDayjs(new Date(currentYear, currentMonth, 1));
|
||||
|
||||
// Calculate the day offset for Monday (isoWeekday() returns 1 for Monday)
|
||||
const startDayOfWeek = startOfMonth.isoWeekday(); // 1 for Monday, 7 for Sunday
|
||||
|
|
@ -314,7 +315,7 @@ export const MonthView = () => {
|
|||
className="text-center items-center justify-center flex min-h-[100px]"
|
||||
>
|
||||
<CalendarColumn
|
||||
getDate={dayjs(date.day).endOf('day')}
|
||||
getDate={newDayjs(date.day).endOf('day')}
|
||||
randomHour={true}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -390,7 +391,9 @@ export const CalendarColumn: FC<{
|
|||
|
||||
const isBeforeNow = useMemo(() => {
|
||||
const originalUtc = getDate.startOf('hour');
|
||||
return originalUtc.startOf('hour').isBefore(dayjs().startOf('hour').utc());
|
||||
return originalUtc
|
||||
.startOf('hour')
|
||||
.isBefore(newDayjs().startOf('hour').utc());
|
||||
}, [getDate, num]);
|
||||
|
||||
const { start, stop } = useInterval(
|
||||
|
|
@ -460,9 +463,12 @@ export const CalendarColumn: FC<{
|
|||
? ExistingDataContextProvider
|
||||
: Fragment;
|
||||
modal.openModal({
|
||||
id: 'add-edit-modal',
|
||||
closeOnClickOutside: false,
|
||||
removeLayout: true,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
askClose: true,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] text-textColor',
|
||||
},
|
||||
|
|
@ -514,28 +520,24 @@ export const CalendarColumn: FC<{
|
|||
? undefined
|
||||
: await new Promise((resolve) => {
|
||||
modal.openModal({
|
||||
title: '',
|
||||
title: t('select_set', 'Select a Set'),
|
||||
closeOnClickOutside: true,
|
||||
askClose: true,
|
||||
closeOnEscape: true,
|
||||
withCloseButton: false,
|
||||
withCloseButton: true,
|
||||
onClose: () => resolve('exit'),
|
||||
classNames: {
|
||||
modal: 'text-textColor',
|
||||
},
|
||||
children: (
|
||||
<ModalWrapperComponent title={t('select_set', 'Select a Set')}>
|
||||
<SetSelectionModal
|
||||
sets={sets}
|
||||
onSelect={(selectedSet) => {
|
||||
resolve(selectedSet);
|
||||
modal.closeAll();
|
||||
}}
|
||||
onContinueWithoutSet={() => {
|
||||
resolve(undefined);
|
||||
modal.closeAll();
|
||||
}}
|
||||
/>
|
||||
</ModalWrapperComponent>
|
||||
<SetSelectionModal
|
||||
sets={sets}
|
||||
onSelect={(selectedSet) => {
|
||||
resolve(selectedSet);
|
||||
modal.closeAll();
|
||||
}}
|
||||
onContinueWithoutSet={() => {
|
||||
resolve(undefined);
|
||||
modal.closeAll();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
});
|
||||
|
|
@ -546,9 +548,12 @@ export const CalendarColumn: FC<{
|
|||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
removeLayout: true,
|
||||
askClose: true,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] text-textColor',
|
||||
},
|
||||
id: 'add-edit-modal',
|
||||
children: (
|
||||
<AddEditModal
|
||||
allIntegrations={integrations.map((p) => ({
|
||||
|
|
@ -571,8 +576,8 @@ export const CalendarColumn: FC<{
|
|||
randomHour
|
||||
? getDate.hour(Math.floor(Math.random() * 24))
|
||||
: getDate.format('YYYY-MM-DDTHH:mm:ss') ===
|
||||
dayjs().startOf('hour').format('YYYY-MM-DDTHH:mm:ss')
|
||||
? dayjs().add(10, 'minute')
|
||||
newDayjs().startOf('hour').format('YYYY-MM-DDTHH:mm:ss')
|
||||
? newDayjs().add(10, 'minute')
|
||||
: getDate
|
||||
}
|
||||
{...(set?.content ? { set: JSON.parse(set.content) } : {})}
|
||||
|
|
@ -585,17 +590,14 @@ export const CalendarColumn: FC<{
|
|||
const openStatistics = useCallback(
|
||||
(id: string) => () => {
|
||||
modal.openModal({
|
||||
title: t('statistics', 'Statistics'),
|
||||
closeOnClickOutside: true,
|
||||
closeOnEscape: true,
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px]',
|
||||
},
|
||||
children: (
|
||||
<ModalWrapperComponent title={t('statistics', 'Statistics')}>
|
||||
<StatisticsModal postId={id} />
|
||||
</ModalWrapperComponent>
|
||||
),
|
||||
children: <StatisticsModal postId={id} />,
|
||||
size: '80%',
|
||||
// title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`,
|
||||
});
|
||||
|
|
@ -911,7 +913,7 @@ const CalendarItem: FC<{
|
|||
</div>
|
||||
<div className="w-full relative">
|
||||
<div className="absolute top-0 start-0 w-full text-ellipsis break-words line-clamp-1 text-left">
|
||||
{stripHtmlValidation('none', post.content, false, true) ||
|
||||
{stripHtmlValidation('none', post.content, false, true, false) ||
|
||||
'no content'}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import clsx from 'clsx';
|
||||
|
|
|
|||
|
|
@ -40,9 +40,14 @@ export const ContinueIntegration: FC<{
|
|||
|
||||
const data = await fetch(`/integrations/social/${provider}/connect`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({...modifiedParams, timezone}),
|
||||
body: JSON.stringify({ ...modifiedParams, timezone }),
|
||||
});
|
||||
|
||||
if (data.status === HttpStatusCode.PreconditionFailed) {
|
||||
push(`/launches?precondition=true`);
|
||||
return ;
|
||||
}
|
||||
|
||||
if (data.status === HttpStatusCode.NotAcceptable) {
|
||||
const { msg } = await data.json();
|
||||
push(`/launches?msg=${msg}`);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Autocomplete } from '@mantine/core';
|
||||
import useSWR from 'swr';
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ import { useCallback } from 'react';
|
|||
import { SelectCustomer } from '@gitroom/frontend/components/launches/select.customer';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import i18next from 'i18next';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
|
||||
// Helper function to get start and end dates based on display type
|
||||
function getDateRange(
|
||||
display: 'day' | 'week' | 'month',
|
||||
referenceDate?: string
|
||||
) {
|
||||
const date = referenceDate ? dayjs(referenceDate) : dayjs();
|
||||
const date = referenceDate ? newDayjs(referenceDate) : newDayjs();
|
||||
|
||||
switch (display) {
|
||||
case 'day':
|
||||
|
|
@ -44,8 +45,8 @@ export const Filters = () => {
|
|||
|
||||
// Calculate display date range text
|
||||
const getDisplayText = () => {
|
||||
const startDate = dayjs(calendar.startDate);
|
||||
const endDate = dayjs(calendar.endDate);
|
||||
const startDate = newDayjs(calendar.startDate);
|
||||
const endDate = newDayjs(calendar.endDate);
|
||||
|
||||
switch (calendar.display) {
|
||||
case 'day':
|
||||
|
|
@ -60,7 +61,7 @@ export const Filters = () => {
|
|||
};
|
||||
|
||||
const setToday = useCallback(() => {
|
||||
const today = dayjs();
|
||||
const today = newDayjs();
|
||||
const currentRange = getDateRange(
|
||||
calendar.display as 'day' | 'week' | 'month'
|
||||
);
|
||||
|
|
@ -151,7 +152,7 @@ export const Filters = () => {
|
|||
);
|
||||
|
||||
const next = useCallback(() => {
|
||||
const currentStart = dayjs(calendar.startDate);
|
||||
const currentStart = newDayjs(calendar.startDate);
|
||||
let nextStart: dayjs.Dayjs;
|
||||
|
||||
switch (calendar.display) {
|
||||
|
|
@ -181,7 +182,7 @@ export const Filters = () => {
|
|||
}, [calendar]);
|
||||
|
||||
const previous = useCallback(() => {
|
||||
const currentStart = dayjs(calendar.startDate);
|
||||
const currentStart = newDayjs(calendar.startDate);
|
||||
let prevStart: dayjs.Dayjs;
|
||||
|
||||
switch (calendar.display) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,16 @@ export const GeneralPreviewComponent: FC<{
|
|||
const mediaDir = useMediaDirectory();
|
||||
|
||||
const renderContent = topValue.map((p) => {
|
||||
const newContent = stripHtmlValidation('normal', p.content, true);
|
||||
const newContent = stripHtmlValidation(
|
||||
'normal',
|
||||
p.content.replace(
|
||||
/<span.*?data-mention-id="([.\s\S]*?)"[.\s\S]*?>([.\s\S]*?)<\/span>/gi,
|
||||
(match, match1, match2) => {
|
||||
return `[[[${match2}]]]`;
|
||||
}
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
const { start, end } = textSlicer(
|
||||
integration?.identifier || '',
|
||||
|
|
@ -27,21 +36,13 @@ export const GeneralPreviewComponent: FC<{
|
|||
const finalValue =
|
||||
newContent
|
||||
.slice(start, end)
|
||||
.replace(/(@.+?)(\s)/gi, (match, match1, match2) => {
|
||||
return `<span class="font-bold" style="color: #ae8afc">${match1.trim()}${match2}</span>`;
|
||||
})
|
||||
.replace(/@\[(.+?)]\((.+?)\)/gi, (match, name, id) => {
|
||||
return `<span class="font-bold" style="color: #ae8afc">@${name}</span>`;
|
||||
.replace(/\[\[\[([.\s\S]*?)]]]/, (match, match1) => {
|
||||
return `<span class="font-bold font-[arial]" style="color: #ae8afc">${match1}</span>`;
|
||||
}) +
|
||||
`<mark class="bg-red-500" data-tooltip-id="tooltip" data-tooltip-content="This text will be cropped">` +
|
||||
newContent
|
||||
.slice(end)
|
||||
.replace(/(@.+?)(\s)/gi, (match, match1, match2) => {
|
||||
return `<span class="font-bold" style="color: #ae8afc">${match1.trim()}${match2}</span>`;
|
||||
})
|
||||
.replace(/@\[(.+?)]\((.+?)\)/gi, (match, name, id) => {
|
||||
return `<span class="font-bold" style="color: #ae8afc">@${name}</span>`;
|
||||
}) +
|
||||
newContent.slice(end).replace(/\[\[\[([.\s\S]*?)]]]/, (match, match1) => {
|
||||
return `<span class="font-bold font-[arial]" style="color: #ae8afc">${match1}</span>`;
|
||||
}) +
|
||||
`</mark>`;
|
||||
|
||||
return { text: finalValue, images: p.image };
|
||||
|
|
@ -110,10 +111,7 @@ export const GeneralPreviewComponent: FC<{
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'text-wrap whitespace-pre',
|
||||
'preview'
|
||||
)}
|
||||
className={clsx('text-wrap whitespace-pre', 'preview')}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: value.text,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { FC, useCallback, useMemo, useState } from 'react';
|
|||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
|
|
@ -153,9 +153,12 @@ const FirstStep: FC = (props) => {
|
|||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
removeLayout: true,
|
||||
askClose: true,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor',
|
||||
},
|
||||
id: 'add-edit-modal',
|
||||
children: (
|
||||
<AddEditModal
|
||||
allIntegrations={integrations.map((p) => ({
|
||||
|
|
@ -301,7 +304,7 @@ export const GeneratorComponent = () => {
|
|||
return;
|
||||
}
|
||||
modal.openModal({
|
||||
title: '',
|
||||
title: 'Generate Posts',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
|
|
@ -309,9 +312,7 @@ export const GeneratorComponent = () => {
|
|||
size: 'xl',
|
||||
children: (
|
||||
<CalendarWeekProvider {...all}>
|
||||
<ModalWrapperComponent title="Generate Posts">
|
||||
<GeneratorPopup />
|
||||
</ModalWrapperComponent>
|
||||
<GeneratorPopup />
|
||||
</CalendarWeekProvider>
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useClickOutside } from '@mantine/hooks';
|
|||
import { Button } from '@gitroom/react/form/button';
|
||||
import { isUSCitizen } from './isuscitizen.utils';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
export const DatePicker: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
onChange: (day: dayjs.Dayjs) => void;
|
||||
|
|
@ -22,10 +23,10 @@ export const DatePicker: FC<{
|
|||
const changeDate = useCallback(
|
||||
(type: 'date' | 'time') => (day: Date) => {
|
||||
onChange(
|
||||
dayjs(
|
||||
newDayjs(
|
||||
type === 'time'
|
||||
? date.format('YYYY-MM-DD') + ' ' + dayjs(day).format('HH:mm:ss')
|
||||
: dayjs(day).format('YYYY-MM-DD') + ' ' + date.format('HH:mm:ss')
|
||||
? date.format('YYYY-MM-DD') + ' ' + newDayjs(day).format('HH:mm:ss')
|
||||
: newDayjs(day).format('YYYY-MM-DD') + ' ' + date.format('HH:mm:ss')
|
||||
)
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const isUSCitizen = () => {
|
||||
const userLanguage = navigator.language || navigator.languages[0];
|
||||
return userLanguage.startsWith('en-US');
|
||||
const userLanguage = localStorage.getItem('isUS') || ((navigator.language || navigator.languages[0]).startsWith('en-US') ? 'US' : 'GLOBAL');
|
||||
return userLanguage === 'US';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
const postUrlEmitter = new EventEmitter();
|
||||
|
||||
export const MediaSettingsLayout = () => {
|
||||
|
|
@ -97,7 +98,8 @@ export const CreateThumbnail: FC<{
|
|||
altText?: string;
|
||||
onAltTextChange?: (altText: string) => void;
|
||||
}> = (props) => {
|
||||
const { onSelect, media, altText, onAltTextChange } = props;
|
||||
const { onSelect, media } = props;
|
||||
const { backendUrl } = useVariables();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
|
|
@ -106,16 +108,12 @@ export const CreateThumbnail: FC<{
|
|||
const [isCapturing, setIsCapturing] = useState(false);
|
||||
|
||||
const handleLoadedMetadata = useCallback(() => {
|
||||
if (videoRef.current) {
|
||||
setDuration(videoRef.current.duration);
|
||||
setIsLoaded(true);
|
||||
}
|
||||
setDuration(videoRef?.current?.duration);
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
const handleTimeUpdate = useCallback(() => {
|
||||
if (videoRef.current) {
|
||||
setCurrentTime(videoRef.current.currentTime);
|
||||
}
|
||||
setCurrentTime(videoRef?.current?.currentTime);
|
||||
}, []);
|
||||
|
||||
const handleSeek = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
|
@ -127,8 +125,6 @@ export const CreateThumbnail: FC<{
|
|||
}, []);
|
||||
|
||||
const captureFrame = useCallback(async () => {
|
||||
if (!videoRef.current || !canvasRef.current) return;
|
||||
|
||||
setIsCapturing(true);
|
||||
|
||||
try {
|
||||
|
|
@ -217,7 +213,9 @@ export const CreateThumbnail: FC<{
|
|||
<div className="relative bg-black rounded-lg overflow-hidden">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={media.path}
|
||||
src={
|
||||
backendUrl + '/public/stream?url=' + encodeURIComponent(media.path)
|
||||
}
|
||||
className="w-full h-[200px] object-contain"
|
||||
onLoadedMetadata={handleLoadedMetadata}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
|
|
@ -299,7 +297,14 @@ export const MediaComponentInner: FC<{
|
|||
alt: string;
|
||||
}) => void;
|
||||
media:
|
||||
| { id: string; name: string; path: string; thumbnail: string; alt: string, thumbnailTimestamp?: number }
|
||||
| {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
thumbnail: string;
|
||||
alt: string;
|
||||
thumbnailTimestamp?: number;
|
||||
}
|
||||
| undefined;
|
||||
}> = (props) => {
|
||||
const { onClose, onSelect, media } = props;
|
||||
|
|
@ -357,163 +362,132 @@ export const MediaComponentInner: FC<{
|
|||
}, [altText, newThumbnail, thumbnail, thumbnailTimestamp]);
|
||||
|
||||
return (
|
||||
<div className="text-textColor fixed start-0 top-0 bg-primary/80 z-[300] w-full h-full p-[60px] animate-fade justify-center flex bg-black/40">
|
||||
<div className="w-full h-full relative">
|
||||
<div className="w-[500px] bg-sixth border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] absolute left-[50%] top-[100px] -translate-x-[50%]">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<TopTitle title={'Media Setting'} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="outline-none absolute end-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-primary hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-[10px] flex flex-col gap-[20px]">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm text-textColor font-medium">
|
||||
Alt Text (for accessibility)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={altText}
|
||||
onChange={(e) => setAltText(e.target.value)}
|
||||
placeholder="Describe the image/video content..."
|
||||
className="w-full px-3 py-2 bg-fifth border border-tableBorder rounded-lg text-textColor placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-forth focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
{media?.path.indexOf('mp4') > -1 && (
|
||||
<>
|
||||
{/* Alt Text Input */}
|
||||
<div>
|
||||
{!isEditingThumbnail ? (
|
||||
<div className="flex flex-col">
|
||||
{/* Show existing thumbnail if it exists */}
|
||||
{(newThumbnail || thumbnail) && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<span className="text-sm text-textColor">
|
||||
Current Thumbnail:
|
||||
</span>
|
||||
<img
|
||||
src={newThumbnail || thumbnail}
|
||||
alt="Current thumbnail"
|
||||
className="max-w-full max-h-[500px] object-contain rounded-lg border border-tableBorder"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-[10px] flex flex-col gap-[20px]">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm text-textColor font-medium">
|
||||
Alt Text (for accessibility)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={altText}
|
||||
onChange={(e) => setAltText(e.target.value)}
|
||||
placeholder="Describe the image/video content..."
|
||||
className="w-full px-3 py-2 bg-fifth border border-tableBorder rounded-lg text-textColor placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-forth focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
{media?.path.indexOf('mp4') > -1 && (
|
||||
<>
|
||||
{/* Alt Text Input */}
|
||||
<div>
|
||||
{!isEditingThumbnail ? (
|
||||
<div className="flex flex-col">
|
||||
{/* Show existing thumbnail if it exists */}
|
||||
{(newThumbnail || thumbnail) && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<span className="text-sm text-textColor">
|
||||
Current Thumbnail:
|
||||
</span>
|
||||
<img
|
||||
src={newThumbnail || thumbnail}
|
||||
alt="Current thumbnail"
|
||||
className="max-w-full max-h-[500px] object-contain rounded-lg border border-tableBorder"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={() => setIsEditingThumbnail(true)}
|
||||
className="bg-third text-textColor px-6 py-2 rounded-lg hover:bg-opacity-80 transition-all flex-1 border border-tableBorder"
|
||||
>
|
||||
{media.thumbnail || newThumbnail
|
||||
? 'Edit Thumbnail'
|
||||
: 'Create Thumbnail'}
|
||||
</button>
|
||||
{(thumbnail || newThumbnail) && (
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setNewThumbnail(null);
|
||||
setThumbnail(null);
|
||||
}}
|
||||
className="bg-red-600 text-white px-6 py-2 rounded-lg hover:bg-opacity-80 transition-all flex-1 border border-red-700"
|
||||
>
|
||||
Clear Thumbnail
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Back button */}
|
||||
<div className="flex justify-start">
|
||||
<button
|
||||
onClick={() => setIsEditingThumbnail(false)}
|
||||
className="text-textColor hover:text-white transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 12H5M12 19L5 12L12 5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Editor */}
|
||||
<CreateThumbnail
|
||||
onSelect={(blob: Blob, timestampMs: number) => {
|
||||
// Convert blob to base64 or handle as needed
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
// You can handle the result here - for now just call onSelect with the blob URL
|
||||
const url = URL.createObjectURL(blob);
|
||||
setNewThumbnail(url);
|
||||
setThumbnailTimestamp(timestampMs);
|
||||
setIsEditingThumbnail(false);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}}
|
||||
media={media}
|
||||
altText={altText}
|
||||
onAltTextChange={setAltText}
|
||||
/>
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={() => setIsEditingThumbnail(true)}
|
||||
className="bg-third text-textColor px-6 py-2 rounded-lg hover:bg-opacity-80 transition-all flex-1 border border-tableBorder"
|
||||
>
|
||||
{media.thumbnail || newThumbnail
|
||||
? 'Edit Thumbnail'
|
||||
: 'Create Thumbnail'}
|
||||
</button>
|
||||
{(thumbnail || newThumbnail) && (
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setNewThumbnail(null);
|
||||
setThumbnail(null);
|
||||
}}
|
||||
className="bg-red-600 text-white px-6 py-2 rounded-lg hover:bg-opacity-80 transition-all flex-1 border border-red-700"
|
||||
>
|
||||
Clear Thumbnail
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Back button */}
|
||||
<div className="flex justify-start">
|
||||
<button
|
||||
onClick={() => setIsEditingThumbnail(false)}
|
||||
className="text-textColor hover:text-white transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 12H5M12 19L5 12L12 5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!isEditingThumbnail && (
|
||||
<div className="flex space-x-2 !mt-[20px]">
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={onClose}
|
||||
className="flex-1 bg-gray-600 text-white px-6 py-2 rounded-lg hover:bg-opacity-80 transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={save}
|
||||
className="flex-1 bg-forth text-white px-6 py-2 rounded-lg hover:bg-opacity-80 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
{/* Thumbnail Editor */}
|
||||
<CreateThumbnail
|
||||
onSelect={(blob: Blob, timestampMs: number) => {
|
||||
// Convert blob to base64 or handle as needed
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
// You can handle the result here - for now just call onSelect with the blob URL
|
||||
const url = URL.createObjectURL(blob);
|
||||
setNewThumbnail(url);
|
||||
setThumbnailTimestamp(timestampMs);
|
||||
setIsEditingThumbnail(false);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}}
|
||||
media={media}
|
||||
altText={altText}
|
||||
onAltTextChange={setAltText}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isEditingThumbnail && (
|
||||
<div className="flex space-x-2 !mt-[20px]">
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={onClose}
|
||||
className="flex-1 bg-gray-600 text-white px-6 py-2 rounded-lg hover:bg-opacity-80 transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={save}
|
||||
className="flex-1 bg-forth text-white px-6 py-2 rounded-lg hover:bg-opacity-80 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -220,14 +220,14 @@ export const PickPlatforms: FC<{
|
|||
ref={ref}
|
||||
>
|
||||
<div className="innerComponent">
|
||||
<div className="flex">
|
||||
<div className="flex gap-[10px] flex-wrap">
|
||||
{integrations
|
||||
.filter((f) => !f.inBetweenSteps)
|
||||
.map((integration) =>
|
||||
!props.singleSelect ? (
|
||||
<div
|
||||
key={integration.id}
|
||||
className="flex gap-[8px] items-center me-[10px]"
|
||||
className="flex gap-[8px] items-center"
|
||||
{...(props.toolTip && {
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content': integration.name,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ export const useIntegrationList = () => {
|
|||
}, []);
|
||||
|
||||
return useSWR('/integrations/list', load, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
fallbackData: [],
|
||||
});
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import dayjs from 'dayjs';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
export const IntegrationContext = createContext<{
|
||||
date: dayjs.Dayjs;
|
||||
integration: Integrations | undefined;
|
||||
|
|
@ -18,7 +19,7 @@ export const IntegrationContext = createContext<{
|
|||
}>({
|
||||
integration: undefined,
|
||||
value: [],
|
||||
date: dayjs(),
|
||||
date: newDayjs(),
|
||||
allIntegrations: [],
|
||||
});
|
||||
export const useIntegration = () => useContext(IntegrationContext);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { useClickOutside } from '@mantine/hooks';
|
|||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { TimeTable } from '@gitroom/frontend/components/launches/time.table';
|
||||
import {
|
||||
Integrations,
|
||||
|
|
@ -137,18 +137,12 @@ export const Menu: FC<{
|
|||
(integration) => integration.id === id
|
||||
);
|
||||
modal.openModal({
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[600px] bg-transparent text-textColor',
|
||||
},
|
||||
size: '100%',
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
closeOnClickOutside: false,
|
||||
children: (
|
||||
<ModalWrapperComponent title="Time Table Slots" ask={true}>
|
||||
<TimeTable integration={findIntegration!} mutate={mutate} />
|
||||
</ModalWrapperComponent>
|
||||
),
|
||||
askClose: true,
|
||||
title: 'Time Table Slots',
|
||||
children: <TimeTable integration={findIntegration!} mutate={mutate} />,
|
||||
});
|
||||
setShow(false);
|
||||
}, [integrations]);
|
||||
|
|
@ -175,9 +169,12 @@ export const Menu: FC<{
|
|||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
removeLayout: true,
|
||||
askClose: true,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor',
|
||||
},
|
||||
id: 'add-edit-modal',
|
||||
children: (
|
||||
<AddEditModal
|
||||
allIntegrations={integrations.map((p) => ({
|
||||
|
|
@ -254,40 +251,36 @@ export const Menu: FC<{
|
|||
classNames: {
|
||||
modal: 'md',
|
||||
},
|
||||
title: '',
|
||||
title: 'Move / Add to customer',
|
||||
withCloseButton: false,
|
||||
closeOnEscape: true,
|
||||
closeOnClickOutside: true,
|
||||
children: (
|
||||
<ModalWrapperComponent title="Move / Add to customer">
|
||||
<CustomerModal
|
||||
// @ts-ignore
|
||||
integration={findIntegration}
|
||||
onClose={() => {
|
||||
mutate();
|
||||
toast.show('Customer Updated', 'success');
|
||||
}}
|
||||
/>
|
||||
</ModalWrapperComponent>
|
||||
<CustomerModal
|
||||
// @ts-ignore
|
||||
integration={findIntegration}
|
||||
onClose={() => {
|
||||
mutate();
|
||||
toast.show('Customer Updated', 'success');
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
setShow(false);
|
||||
}, [integrations]);
|
||||
const updateCredentials = useCallback(() => {
|
||||
modal.openModal({
|
||||
title: '',
|
||||
title: 'Custom URL',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'md',
|
||||
},
|
||||
children: (
|
||||
<ModalWrapperComponent title="Custom URL">
|
||||
<CustomVariables
|
||||
identifier={findIntegration.identifier}
|
||||
gotoUrl={(url: string) => router.push(url)}
|
||||
variables={findIntegration.customFields}
|
||||
/>
|
||||
</ModalWrapperComponent>
|
||||
<CustomVariables
|
||||
identifier={findIntegration.identifier}
|
||||
gotoUrl={(url: string) => router.push(url)}
|
||||
variables={findIntegration.customFields}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
|
@ -21,7 +21,7 @@ export const NewPost = () => {
|
|||
? undefined
|
||||
: await new Promise((resolve) => {
|
||||
modal.openModal({
|
||||
title: '',
|
||||
title: t('select_set', 'Select a Set'),
|
||||
closeOnClickOutside: true,
|
||||
closeOnEscape: true,
|
||||
withCloseButton: false,
|
||||
|
|
@ -30,19 +30,17 @@ export const NewPost = () => {
|
|||
modal: 'text-textColor',
|
||||
},
|
||||
children: (
|
||||
<ModalWrapperComponent title={t('select_set', 'Select a Set')}>
|
||||
<SetSelectionModal
|
||||
sets={sets}
|
||||
onSelect={(selectedSet) => {
|
||||
resolve(selectedSet);
|
||||
modal.closeAll();
|
||||
}}
|
||||
onContinueWithoutSet={() => {
|
||||
resolve(undefined);
|
||||
modal.closeAll();
|
||||
}}
|
||||
/>
|
||||
</ModalWrapperComponent>
|
||||
<SetSelectionModal
|
||||
sets={sets}
|
||||
onSelect={(selectedSet) => {
|
||||
resolve(selectedSet);
|
||||
modal.closeAll();
|
||||
}}
|
||||
onContinueWithoutSet={() => {
|
||||
resolve(undefined);
|
||||
modal.closeAll();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
});
|
||||
|
|
@ -53,9 +51,12 @@ export const NewPost = () => {
|
|||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
removeLayout: true,
|
||||
askClose: true,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor',
|
||||
},
|
||||
id: 'add-edit-modal',
|
||||
children: (
|
||||
<AddEditModal
|
||||
allIntegrations={integrations.map((p) => ({
|
||||
|
|
|
|||
|
|
@ -56,10 +56,12 @@ const ActionControls = ({ store }: any) => {
|
|||
body: formData,
|
||||
})
|
||||
).json();
|
||||
close.setMedia([{
|
||||
id: data.id,
|
||||
path: data.path,
|
||||
}]);
|
||||
close.setMedia([
|
||||
{
|
||||
id: data.id,
|
||||
path: data.path,
|
||||
},
|
||||
]);
|
||||
close.close();
|
||||
}}
|
||||
>
|
||||
|
|
@ -102,63 +104,34 @@ const Polonto: FC<{
|
|||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="fixed start-0 top-0 bg-primary/80 z-[300] w-full min-h-full px-[60px] animate-fade">
|
||||
<div className="w-full h-full bg-sixth border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<TopTitle title="Design Media" />
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="outline-none absolute end-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-primary hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white text-black relative z-[400] polonto">
|
||||
<CloseContext.Provider
|
||||
value={{
|
||||
close: () => closeModal(),
|
||||
setMedia,
|
||||
}}
|
||||
>
|
||||
<PolotnoContainer
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '700px',
|
||||
<div className="bg-white text-black relative z-[400] polonto">
|
||||
<CloseContext.Provider
|
||||
value={{
|
||||
close: () => closeModal(),
|
||||
setMedia,
|
||||
}}
|
||||
>
|
||||
<PolotnoContainer
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '700px',
|
||||
}}
|
||||
>
|
||||
<SidePanelWrap>
|
||||
<SidePanel store={store} sections={features} />
|
||||
</SidePanelWrap>
|
||||
<WorkspaceWrap>
|
||||
<Toolbar
|
||||
store={store}
|
||||
components={{
|
||||
ActionControls,
|
||||
}}
|
||||
>
|
||||
<SidePanelWrap>
|
||||
<SidePanel store={store} sections={features} />
|
||||
</SidePanelWrap>
|
||||
<WorkspaceWrap>
|
||||
<Toolbar
|
||||
store={store}
|
||||
components={{
|
||||
ActionControls,
|
||||
}}
|
||||
/>
|
||||
<Workspace store={store} />
|
||||
<ZoomButtons store={store} />
|
||||
</WorkspaceWrap>
|
||||
</PolotnoContainer>
|
||||
</CloseContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
<Workspace store={store} />
|
||||
<ZoomButtons store={store} />
|
||||
</WorkspaceWrap>
|
||||
</PolotnoContainer>
|
||||
</CloseContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FC, Fragment, useCallback } from 'react';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
|
|
|
|||
|
|
@ -32,7 +32,14 @@ export const TagsComponent: FC<{
|
|||
name: string;
|
||||
color: string;
|
||||
}[];
|
||||
}>('tags', loadTags);
|
||||
}>('tags', loadTags, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
});
|
||||
const onDelete = useCallback(
|
||||
(tagIndex: number) => {
|
||||
const modify = tagValue.filter((_, i) => i !== tagIndex);
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import timezone from 'dayjs/plugin/timezone';
|
|||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
// @ts-ignore
|
||||
import useKeypress from 'react-use-keypress';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { sortBy } from 'lodash';
|
||||
import { usePreventWindowUnload } from '@gitroom/react/helpers/use.prevent.window.unload';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const hours = [...Array(24).keys()].map((i, index) => ({
|
||||
|
|
@ -70,12 +71,12 @@ export const TimeTable: FC<{
|
|||
);
|
||||
const addHour = useCallback(() => {
|
||||
const calculateMinutes =
|
||||
dayjs()
|
||||
newDayjs()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.add(hour, 'hours')
|
||||
.add(minute, 'minutes')
|
||||
.diff(dayjs().utc().startOf('day'), 'minutes') - dayjs.tz().utcOffset();
|
||||
.diff(newDayjs().utc().startOf('day'), 'minutes') - dayjs.tz().utcOffset();
|
||||
setCurrentTimes((prev) => [
|
||||
...prev,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import React, { FC, useMemo, useState, useCallback, useEffect } from 'react';
|
|||
import { Web3ProviderInterface } from '@gitroom/frontend/components/launches/web3/web3.provider.interface';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import { ButtonCaster } from '@gitroom/frontend/components/auth/providers/farcaster.provider';
|
||||
export const WrapcasterProvider: FC<Web3ProviderInterface> = (props) => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import '@neynar/react/dist/style.css';
|
|||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Web3ProviderInterface } from '@gitroom/frontend/components/launches/web3/web3.provider.interface';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import React, { FC, useMemo, useState, useCallback, useEffect } from 'react';
|
|||
import { Web3ProviderInterface } from '@gitroom/frontend/components/launches/web3/web3.provider.interface';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import {
|
||||
NeynarAuthButton,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import dayjs from 'dayjs';
|
|||
import useSWR, { useSWRConfig } from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { continueProviderList } from '@gitroom/frontend/components/new-launch/providers/continue-provider/list';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
export const Null: FC<{
|
||||
closeModal: () => void;
|
||||
existingId: string[];
|
||||
|
|
@ -22,6 +23,12 @@ export const ContinueProvider: FC = () => {
|
|||
return list;
|
||||
}, []);
|
||||
const { data: integrations } = useSWR('/integrations/list', load, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
fallbackData: [],
|
||||
});
|
||||
const closeModal = useCallback(() => {
|
||||
|
|
@ -76,7 +83,7 @@ export const ContinueProvider: FC = () => {
|
|||
<div className="pt-[16px] max-h-[600px] overflow-hidden overflow-y-auto">
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
date: dayjs(),
|
||||
date: newDayjs(),
|
||||
value: [],
|
||||
allIntegrations: [],
|
||||
integration: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import {
|
||||
cookieName,
|
||||
fallbackLng,
|
||||
|
|
@ -70,7 +70,7 @@ export const ChangeLanguageComponent = () => {
|
|||
const handleLanguageChange = (language: string) => {
|
||||
setCookie(language);
|
||||
i18next.changeLanguage(language);
|
||||
modals.closeModal('change-language');
|
||||
modals.closeCurrent();
|
||||
};
|
||||
|
||||
// Function to get language name in its native script
|
||||
|
|
@ -123,16 +123,9 @@ export const LanguageComponent = () => {
|
|||
const t = useT();
|
||||
const openModal = () => {
|
||||
modal.openModal({
|
||||
title: '',
|
||||
withCloseButton: false,
|
||||
modalId: 'change-language',
|
||||
children: (
|
||||
<ModalWrapperComponent title={t('change_language', 'Change Language')}>
|
||||
<ChangeLanguageComponent />
|
||||
</ModalWrapperComponent>
|
||||
),
|
||||
size: 'lg',
|
||||
centered: true,
|
||||
title: t('change_language', 'Change Language'),
|
||||
withCloseButton: true,
|
||||
children: <ChangeLanguageComponent />,
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -73,11 +73,13 @@ function LayoutContextInner(params: { children: ReactNode }) {
|
|||
: '/analytics?onboarding=true';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (response?.headers?.get('reload')) {
|
||||
window.location.reload();
|
||||
return true;
|
||||
}
|
||||
if (response.status === 401) {
|
||||
|
||||
if (response.status === 401 || response?.headers?.get('logout')) {
|
||||
if (!isSecured) {
|
||||
setCookie('auth', '', -10);
|
||||
setCookie('showorg', '', -10);
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
<CopilotKit
|
||||
credentials="include"
|
||||
runtimeUrl={backendUrl + '/copilot/chat'}
|
||||
showDevConsole={false}
|
||||
>
|
||||
<MantineWrapper>
|
||||
{user.tier === 'FREE' && searchParams.get('check') && (
|
||||
|
|
@ -84,7 +85,6 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
<Toaster />
|
||||
<ShowPostSelector />
|
||||
<NewSubscription />
|
||||
{user.tier !== 'FREE' && <Onboarding />}
|
||||
<Support />
|
||||
<ContinueProvider />
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-6 text-textColor flex flex-col">
|
||||
|
|
|
|||
375
apps/frontend/src/components/layout/new-modal.tsx
Normal file
375
apps/frontend/src/components/layout/new-modal.tsx
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
import { create } from 'zustand';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
memo,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import clsx from 'clsx';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
interface OpenModalInterface {
|
||||
title?: string;
|
||||
closeOnClickOutside?: boolean;
|
||||
removeLayout?: boolean;
|
||||
closeOnEscape?: boolean;
|
||||
withCloseButton?: boolean;
|
||||
askClose?: boolean;
|
||||
onClose?: () => void;
|
||||
children: ReactNode | ((close: () => void) => ReactNode);
|
||||
classNames?: {
|
||||
modal?: string;
|
||||
};
|
||||
size?: string | number;
|
||||
height?: string | number;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface ModalManagerStoreInterface {
|
||||
closeById(id: string): void;
|
||||
openModal(params: OpenModalInterface): void;
|
||||
closeAll(): void;
|
||||
}
|
||||
|
||||
interface State extends ModalManagerStoreInterface {
|
||||
modalManager: Array<{ id: string } & OpenModalInterface>;
|
||||
}
|
||||
|
||||
const useModalStore = create<State>((set) => ({
|
||||
modalManager: [],
|
||||
openModal: (params) => {
|
||||
const newId = params.id || makeId(20);
|
||||
set((state) => ({
|
||||
modalManager: [
|
||||
...state.modalManager,
|
||||
...(!state.modalManager.some((p) => p.id === newId)
|
||||
? [{ id: newId, ...params }]
|
||||
: []),
|
||||
],
|
||||
}));
|
||||
},
|
||||
closeById: (id) =>
|
||||
set((state) => ({
|
||||
modalManager: state.modalManager.filter((modal) => modal.id !== id),
|
||||
})),
|
||||
closeAll: () => set({ modalManager: [] }),
|
||||
}));
|
||||
|
||||
const CurrentModalContext = createContext({ id: '' });
|
||||
|
||||
interface ModalManagerInterface extends ModalManagerStoreInterface {
|
||||
closeCurrent(): void;
|
||||
}
|
||||
|
||||
export const useModals = () => {
|
||||
const { closeAll, openModal, closeById } = useModalStore(
|
||||
useShallow((state) => ({
|
||||
openModal: state.openModal,
|
||||
closeById: state.closeById,
|
||||
closeAll: state.closeAll,
|
||||
}))
|
||||
);
|
||||
|
||||
const modalContext = useContext(CurrentModalContext);
|
||||
|
||||
return {
|
||||
openModal,
|
||||
closeAll,
|
||||
closeById,
|
||||
closeCurrent: () => {
|
||||
if (modalContext.id) {
|
||||
closeById(modalContext.id);
|
||||
}
|
||||
},
|
||||
} satisfies ModalManagerInterface;
|
||||
};
|
||||
|
||||
export const Component: FC<{
|
||||
closeModal: (id: string) => void;
|
||||
zIndex: number;
|
||||
isLast: boolean;
|
||||
modal: { id: string } & OpenModalInterface;
|
||||
}> = memo(({ isLast, modal, closeModal, zIndex }) => {
|
||||
const decision = useDecisionModal();
|
||||
const closeModalFunction = useCallback(async () => {
|
||||
if (modal.askClose) {
|
||||
const open = await decision.open();
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
modal?.onClose?.();
|
||||
closeModal(modal.id);
|
||||
}, [modal.id, closeModal]);
|
||||
|
||||
const RenderComponent = useMemo(() => {
|
||||
return typeof modal.children === 'function'
|
||||
? modal.children(closeModalFunction)
|
||||
: modal.children;
|
||||
}, [modal, closeModalFunction]);
|
||||
|
||||
useHotkeys(
|
||||
'Escape',
|
||||
() => {
|
||||
if (isLast) {
|
||||
closeModalFunction();
|
||||
}
|
||||
},
|
||||
[isLast, closeModalFunction]
|
||||
);
|
||||
|
||||
if (modal.removeLayout) {
|
||||
return (
|
||||
<div
|
||||
style={{ zIndex }}
|
||||
className={clsx(
|
||||
'fixed flex left-0 top-0 min-w-full min-h-full bg-popup transition-all animate-fadeIn overflow-y-auto pb-[50px] text-newTextColor',
|
||||
!isLast && '!overflow-hidden'
|
||||
)}
|
||||
>
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute top-0 left-0 min-w-full min-h-full">
|
||||
<div
|
||||
className="mx-auto py-[48px]"
|
||||
{...(modal.size && { style: { width: modal.size } })}
|
||||
>
|
||||
{typeof modal.children === 'function'
|
||||
? modal.children(closeModalFunction)
|
||||
: modal.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CurrentModalContext.Provider value={{ id: modal.id }}>
|
||||
<div
|
||||
onClick={closeModalFunction}
|
||||
style={{ zIndex }}
|
||||
className="fixed flex left-0 top-0 min-w-full min-h-full bg-popup transition-all animate-fadeIn overflow-y-auto pb-[50px] text-newTextColor"
|
||||
>
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute top-0 left-0 min-w-full min-h-full pt-[100px] pb-[100px]">
|
||||
<div
|
||||
className={clsx(
|
||||
!modal.removeLayout && 'gap-[40px] p-[32px]',
|
||||
'bg-newBgColorInner mx-auto flex flex-col w-fit rounded-[24px] relative',
|
||||
modal.size ? '' : 'min-w-[600px]'
|
||||
)}
|
||||
{...(modal.size && { style: { width: modal.size } })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="text-[24px] font-[600] flex-1">
|
||||
{modal.title}
|
||||
</div>
|
||||
{typeof modal.withCloseButton === 'undefined' ||
|
||||
modal.withCloseButton ? (
|
||||
<div className="cursor-pointer">
|
||||
<button
|
||||
className="outline-none absolute end-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
onClick={closeModalFunction}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="whitespace-pre-line">{RenderComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CurrentModalContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export const ModalManagerInner: FC = () => {
|
||||
const { closeModal, modalManager } = useModalStore(
|
||||
useShallow((state) => ({
|
||||
closeModal: state.closeById,
|
||||
modalManager: state.modalManager,
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (modalManager.length > 0) {
|
||||
document.querySelector('body')?.classList.add('overflow-hidden');
|
||||
Array.from(document.querySelectorAll('.blurMe') || []).map((p) =>
|
||||
p.classList.add('blur-xs', 'pointer-events-none')
|
||||
);
|
||||
} else {
|
||||
document.querySelector('body')?.classList.remove('overflow-hidden');
|
||||
Array.from(document.querySelectorAll('.blurMe') || []).map((p) =>
|
||||
p.classList.remove('blur-xs', 'pointer-events-none')
|
||||
);
|
||||
}
|
||||
}, [modalManager]);
|
||||
|
||||
if (modalManager.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`body, html { overflow: hidden !important; }`}</style>
|
||||
{modalManager.map((modal, index) => (
|
||||
<Component
|
||||
isLast={modalManager.length - 1 === index}
|
||||
key={modal.id}
|
||||
modal={modal}
|
||||
zIndex={200 + index}
|
||||
closeModal={closeModal}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const ModalManager: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<div>
|
||||
<ModalManagerEmitter />
|
||||
<ModalManagerInner />
|
||||
<div className="transition-all w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
export const showModalEmitter = (params: ModalManagerInterface) => {
|
||||
emitter.emit('show', params);
|
||||
};
|
||||
|
||||
export const ModalManagerEmitter: FC = () => {
|
||||
const { showModal } = useModalStore(
|
||||
useShallow((state) => ({
|
||||
showModal: state.openModal,
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
emitter.on('show', (params: OpenModalInterface) => {
|
||||
showModal(params);
|
||||
});
|
||||
|
||||
return () => {
|
||||
emitter.removeAllListeners('show');
|
||||
};
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const DecisionModal: FC<{
|
||||
description: string;
|
||||
approveLabel: string;
|
||||
cancelLabel: string;
|
||||
resolution: (value: boolean) => void;
|
||||
}> = ({ description, cancelLabel, approveLabel, resolution }) => {
|
||||
const { closeCurrent } = useModals();
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div>{description}</div>
|
||||
<div className="flex gap-[12px] mt-[16px]">
|
||||
<Button
|
||||
onClick={() => {
|
||||
resolution(true);
|
||||
closeCurrent();
|
||||
}}
|
||||
>
|
||||
{approveLabel}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
resolution(false);
|
||||
closeCurrent();
|
||||
}}
|
||||
>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const decisionModalEmitter = new EventEmitter();
|
||||
|
||||
export const areYouSure = ({
|
||||
title = 'Are you sure?',
|
||||
description = 'Are you sure you want to close this modal?' as any,
|
||||
approveLabel = 'Yes',
|
||||
cancelLabel = 'No',
|
||||
} = {}): Promise<boolean> => {
|
||||
return new Promise<boolean>((newRes) => {
|
||||
decisionModalEmitter.emit('open', {
|
||||
title,
|
||||
description,
|
||||
approveLabel,
|
||||
cancelLabel,
|
||||
newRes,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const DecisionEverywhere: FC = () => {
|
||||
const decision = useDecisionModal();
|
||||
useEffect(() => {
|
||||
decisionModalEmitter.on('open', decision.open);
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useDecisionModal = () => {
|
||||
const modals = useModals();
|
||||
const open = useCallback(
|
||||
({
|
||||
title = 'Are you sure?',
|
||||
description = 'Are you sure you want to close this modal?' as any,
|
||||
approveLabel = 'Yes',
|
||||
cancelLabel = 'No',
|
||||
newRes = undefined as any,
|
||||
} = {}) => {
|
||||
return new Promise<boolean>((res) => {
|
||||
modals.openModal({
|
||||
title,
|
||||
askClose: false,
|
||||
onClose: () => res(false),
|
||||
children: (
|
||||
<DecisionModal
|
||||
resolution={(value) => (newRes ? newRes(value) : res(value))}
|
||||
description={description}
|
||||
approveLabel={approveLabel}
|
||||
cancelLabel={cancelLabel}
|
||||
/>
|
||||
),
|
||||
});
|
||||
});
|
||||
},
|
||||
[modals]
|
||||
);
|
||||
|
||||
return { open };
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ModalWrapperComponent } from '@gitroom/frontend/components/new-launch/modal.wrapper.component';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
|
||||
export const PreConditionComponentModal: FC = () => {
|
||||
const modal = useModals();
|
||||
return (
|
||||
<div className="flex flex-col gap-[16px]">
|
||||
<div className="whitespace-pre-line">
|
||||
This social channel was connected previously to another Postiz account.
|
||||
{'\n'}
|
||||
To continue, please fast-track your trial for an immediate charge.{'\n'}
|
||||
{'\n'}
|
||||
** Please be advised that the account will not eligible for a refund,
|
||||
and the charge is final.
|
||||
</div>
|
||||
<div className="flex gap-[2px] justify-center">
|
||||
<Button
|
||||
onClick={() => (window.location.href = '/billing?finishTrial=true')}
|
||||
>
|
||||
Fast track - Charge me now
|
||||
</Button>
|
||||
<Button onClick={modal.closeCurrent} secondary={true}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const PreConditionComponent: FC = () => {
|
||||
const modal = useModals();
|
||||
const query = useSearchParams();
|
||||
useEffect(() => {
|
||||
if (query.get('precondition')) {
|
||||
modal.openModal({
|
||||
title: 'Suspicious activity detected',
|
||||
withCloseButton: true,
|
||||
classNames: {
|
||||
modal: 'text-textColor',
|
||||
},
|
||||
children: <PreConditionComponentModal />,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
41
apps/frontend/src/components/layout/set.timezone.tsx
Normal file
41
apps/frontend/src/components/layout/set.timezone.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
import dayjs, { ConfigType } from 'dayjs';
|
||||
import { FC, useEffect } from 'react';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(utc);
|
||||
|
||||
const { utc: originalUtc } = dayjs;
|
||||
|
||||
export const getTimezone = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return dayjs.tz.guess();
|
||||
}
|
||||
return localStorage.getItem('timezone') || dayjs.tz.guess();
|
||||
};
|
||||
|
||||
export const newDayjs = (config?: ConfigType) => {
|
||||
return dayjs(config);
|
||||
};
|
||||
|
||||
const SetTimezone: FC = () => {
|
||||
useEffect(() => {
|
||||
dayjs.utc = (config?: ConfigType, format?: string, strict?: boolean) => {
|
||||
const result = originalUtc(config, format, strict);
|
||||
|
||||
// Attach `.local()` method to the returned Dayjs object
|
||||
result.local = function () {
|
||||
return result.tz(getTimezone());
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
if (localStorage.getItem('timezone')) {
|
||||
dayjs.tz.setDefault(getTimezone());
|
||||
}
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SetTimezone;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import React, {
|
||||
FC,
|
||||
Ref,
|
||||
|
|
@ -30,6 +30,7 @@ import { SignaturesComponent } from '@gitroom/frontend/components/settings/signa
|
|||
import { Autopost } from '@gitroom/frontend/components/autopost/autopost';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { SVGLine } from '@gitroom/frontend/components/launches/launches.component';
|
||||
import { GlobalSettings } from '@gitroom/frontend/components/settings/global.settings';
|
||||
export const SettingsPopup: FC<{
|
||||
getRef?: Ref<any>;
|
||||
}> = (props) => {
|
||||
|
|
@ -80,22 +81,12 @@ export const SettingsPopup: FC<{
|
|||
close();
|
||||
}, []);
|
||||
|
||||
const [tab, setTab] = useState(() => {
|
||||
if (user?.tier?.team_members && isGeneral) {
|
||||
return 'teams';
|
||||
}
|
||||
if (user?.tier?.webhooks) {
|
||||
return 'webhooks';
|
||||
}
|
||||
if (user?.tier?.autoPost) {
|
||||
return 'autopost';
|
||||
}
|
||||
return 'sets';
|
||||
});
|
||||
const [tab, setTab] = useState('global_settings');
|
||||
|
||||
const t = useT();
|
||||
const list = useMemo(() => {
|
||||
const arr = [];
|
||||
arr.push({ tab: 'global_settings', label: t('global_settings', 'Global Settings') });
|
||||
// Populate tabs based on user permissions
|
||||
if (user?.tier?.team_members && isGeneral) {
|
||||
arr.push({ tab: 'teams', label: t('teams', 'Teams') });
|
||||
|
|
@ -168,6 +159,11 @@ export const SettingsPopup: FC<{
|
|||
!getRef && 'rounded-[4px]'
|
||||
)}
|
||||
>
|
||||
{tab === 'global_settings' && (
|
||||
<div>
|
||||
<GlobalSettings />
|
||||
</div>
|
||||
)}
|
||||
{tab === 'teams' && !!user?.tier?.team_members && isGeneral && (
|
||||
<div>
|
||||
<TeamsComponent />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { FC, ReactNode } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
|
|
@ -42,6 +41,24 @@ export const useMenuItem = () => {
|
|||
),
|
||||
path: '/launches',
|
||||
},
|
||||
{
|
||||
name: 'Agent',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="23"
|
||||
height="23"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M21.1963 9.07375C20.2913 6.95494 18.6824 5.21364 16.6416 4.14422C14.6009 3.0748 12.2534 2.74287 9.99616 3.20455C7.73891 3.66623 5.71031 4.8932 4.25334 6.67802C2.79637 8.46284 2.0004 10.696 2 13V21.25C2 21.7141 2.18437 22.1592 2.51256 22.4874C2.84075 22.8156 3.28587 23 3.75 23H10.8337C11.6141 24.7821 12.8964 26.2984 14.5241 27.3638C16.1519 28.4293 18.0546 28.9978 20 29H28.25C28.7141 29 29.1592 28.8156 29.4874 28.4874C29.8156 28.1592 30 27.7141 30 27.25V19C29.9995 16.5553 29.1036 14.1955 27.4814 12.3666C25.8593 10.5376 23.6234 9.36619 21.1963 9.07375ZM4 13C4 11.4177 4.46919 9.87103 5.34824 8.55544C6.22729 7.23984 7.47672 6.21446 8.93853 5.60896C10.4003 5.00346 12.0089 4.84504 13.5607 5.15372C15.1126 5.4624 16.538 6.22432 17.6569 7.34314C18.7757 8.46197 19.5376 9.88743 19.8463 11.4393C20.155 12.9911 19.9965 14.5997 19.391 16.0615C18.7855 17.5233 17.7602 18.7727 16.4446 19.6518C15.129 20.5308 13.5823 21 12 21H4V13ZM28 27H20C18.5854 26.9984 17.1964 26.6225 15.974 25.9106C14.7516 25.1986 13.7394 24.1759 13.04 22.9463C14.4096 22.8041 15.7351 22.3804 16.9333 21.7017C18.1314 21.023 19.1763 20.104 20.0024 19.0023C20.8284 17.9006 21.4179 16.6401 21.7337 15.2998C22.0495 13.9595 22.0848 12.5684 21.8375 11.2137C23.5916 11.6277 25.1545 12.6218 26.273 14.035C27.3915 15.4482 28 17.1977 28 19V27Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
path: '/agents',
|
||||
},
|
||||
{
|
||||
name: t('analytics', 'Analytics'),
|
||||
icon: (
|
||||
|
|
@ -237,7 +254,7 @@ export const useMenuItem = () => {
|
|||
</svg>
|
||||
),
|
||||
path: '/settings',
|
||||
role: ['ADMIN', "USER", 'SUPERADMIN'],
|
||||
role: ['ADMIN', 'USER', 'SUPERADMIN'],
|
||||
},
|
||||
] satisfies MenuItemInterface[] as MenuItemInterface[];
|
||||
|
||||
|
|
@ -254,7 +271,7 @@ export const TopMenu: FC = () => {
|
|||
const { isGeneral, billingEnabled } = useVariables();
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-1 flex-col gap-[16px]">
|
||||
<div className="flex flex-1 flex-col gap-[16px] blurMe">
|
||||
{
|
||||
// @ts-ignore
|
||||
user?.orgId &&
|
||||
|
|
@ -286,7 +303,7 @@ export const TopMenu: FC = () => {
|
|||
))
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col gap-[16px]">
|
||||
<div className="flex flex-col gap-[16px] blurMe">
|
||||
{secondMenu
|
||||
.filter((f) => {
|
||||
if (f.hide) {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
import { capitalize, chunk, fill } from 'lodash';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { MarketplaceProvider } from '@gitroom/frontend/components/marketplace/marketplace.provider';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { CustomSelect } from '@gitroom/react/form/custom.select';
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import useSWR from 'swr';
|
|||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { OrderList } from '@gitroom/frontend/components/marketplace/order.list';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { countries } from '@gitroom/nestjs-libraries/services/stripe.country.list';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ import useSWR from 'swr';
|
|||
import { capitalize } from 'lodash';
|
||||
import removeMd from 'remove-markdown';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { Post as PrismaPost } from '@prisma/client';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import dayjs from 'dayjs';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
const PreviewPopupDynamic = dynamic(() =>
|
||||
import('@gitroom/frontend/components/marketplace/preview.popup.dynamic').then(
|
||||
(mod) => mod.PreviewPopupDynamic
|
||||
|
|
@ -279,7 +280,7 @@ export const Post: FC<{
|
|||
<IntegrationContext.Provider
|
||||
value={{
|
||||
allIntegrations: [],
|
||||
date: dayjs(),
|
||||
date: newDayjs(),
|
||||
integration,
|
||||
value: [],
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,13 @@ import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
|||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { ThirdPartyMedia } from '@gitroom/frontend/components/third-parties/third-party.media';
|
||||
import { ReactSortable } from 'react-sortablejs';
|
||||
import { useMediaSettings } from '@gitroom/frontend/components/launches/helpers/media.settings.component';
|
||||
import {
|
||||
MediaComponentInner,
|
||||
useMediaSettings,
|
||||
} from '@gitroom/frontend/components/launches/helpers/media.settings.component';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import { AiVideo } from '@gitroom/frontend/components/launches/ai.video';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
const Polonto = dynamic(
|
||||
() => import('@gitroom/frontend/components/launches/polonto')
|
||||
);
|
||||
|
|
@ -245,6 +249,10 @@ export const MediaBox: FC<{
|
|||
|
||||
const dragAndDrop = useCallback(
|
||||
async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
|
||||
if (!ref?.current?.setOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const clipboardItems = event.map((p) => ({
|
||||
kind: 'file',
|
||||
|
|
@ -560,12 +568,12 @@ export const MultiMediaComponent: FC<{
|
|||
dummy,
|
||||
} = props;
|
||||
const user = useUser();
|
||||
const modals = useModals();
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setCurrentMedia(value);
|
||||
}
|
||||
}, [value]);
|
||||
const [modal, setShowModal] = useState(false);
|
||||
const [mediaModal, setMediaModal] = useState(false);
|
||||
const [currentMedia, setCurrentMedia] = useState(value);
|
||||
const mediaDirectory = useMediaDirectory();
|
||||
|
|
@ -594,17 +602,14 @@ export const MultiMediaComponent: FC<{
|
|||
[currentMedia]
|
||||
);
|
||||
const showModal = useCallback(() => {
|
||||
if (!modal) {
|
||||
onOpen?.();
|
||||
} else {
|
||||
onClose?.();
|
||||
}
|
||||
setShowModal(!modal);
|
||||
}, [modal, onOpen, onClose]);
|
||||
const closeDesignModal = useCallback(() => {
|
||||
onClose?.();
|
||||
setMediaModal(false);
|
||||
}, [modal]);
|
||||
modals.openModal({
|
||||
askClose: false,
|
||||
children: (close) => (
|
||||
<MediaBox setMedia={changeMedia} closeModal={close} />
|
||||
),
|
||||
});
|
||||
}, [changeMedia]);
|
||||
|
||||
const clearMedia = useCallback(
|
||||
(topIndex: number) => () => {
|
||||
const newMedia = currentMedia?.filter((f, index) => index !== topIndex);
|
||||
|
|
@ -618,10 +623,19 @@ export const MultiMediaComponent: FC<{
|
|||
},
|
||||
[currentMedia]
|
||||
);
|
||||
|
||||
const designMedia = useCallback(() => {
|
||||
onOpen?.();
|
||||
setMediaModal(true);
|
||||
}, []);
|
||||
if (!!user?.tier?.ai && !dummy) {
|
||||
modals.openModal({
|
||||
askClose: false,
|
||||
title: 'Design Media',
|
||||
size: '80%',
|
||||
children: (close) => (
|
||||
<Polonto setMedia={changeMedia} closeModal={close} />
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [changeMedia]);
|
||||
|
||||
const mediaSettings = useMediaSettings();
|
||||
|
||||
|
|
@ -629,12 +643,7 @@ export const MultiMediaComponent: FC<{
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[8px] bg-bigStrip rounded-bl-[8px] select-none w-full">
|
||||
{modal && <MediaBox setMedia={changeMedia} closeModal={showModal} />}
|
||||
{mediaModal && !!user?.tier?.ai && !dummy && (
|
||||
<Polonto setMedia={changeMedia} closeModal={closeDesignModal} />
|
||||
)}
|
||||
|
||||
<div className="b1 flex flex-col gap-[8px] bg-bigStrip rounded-bl-[8px] select-none w-full">
|
||||
<div className="flex gap-[10px]">
|
||||
<Button
|
||||
onClick={showModal}
|
||||
|
|
@ -683,17 +692,31 @@ export const MultiMediaComponent: FC<{
|
|||
<div className="w-full h-full relative group">
|
||||
<div
|
||||
onClick={async () => {
|
||||
const data: any = await mediaSettings(media);
|
||||
console.log(
|
||||
value?.map((p) => (p.id === data.id ? data : p))
|
||||
);
|
||||
onChange({
|
||||
target: {
|
||||
name: 'upload',
|
||||
value: value?.map((p) =>
|
||||
p.id === data.id ? data : p
|
||||
),
|
||||
},
|
||||
modals.openModal({
|
||||
title: 'Media Settings',
|
||||
children: (close) => (
|
||||
<MediaComponentInner
|
||||
media={media as any}
|
||||
onClose={close}
|
||||
onSelect={(value: any) => {
|
||||
console.log(value);
|
||||
onChange({
|
||||
target: {
|
||||
name: 'upload',
|
||||
value: currentMedia.map((p) => {
|
||||
if (p.id === media.id) {
|
||||
return {
|
||||
...p,
|
||||
...value,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}),
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
className="absolute top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] bg-black/80 rounded-[10px] opacity-0 group-hover:opacity-100 transition-opacity z-[100]"
|
||||
|
|
@ -734,8 +757,8 @@ export const MultiMediaComponent: FC<{
|
|||
)}
|
||||
</div>
|
||||
{!dummy && (
|
||||
<div className="flex gap-[10px] bg-newBgLineColor w-full">
|
||||
<div className="flex py-[10px]">
|
||||
<div className="flex gap-[10px] bg-newBgLineColor w-full b1">
|
||||
<div className="flex py-[10px] b2">
|
||||
<Button
|
||||
onClick={designMedia}
|
||||
className="ms-[10px] rounded-[4px] gap-[8px] !text-primary justify-center items-center w-[127px] flex border border-dashed border-newBgLineColor bg-newColColor"
|
||||
|
|
|
|||
|
|
@ -88,18 +88,10 @@ export function useUppyUploader(props: {
|
|||
// Expand generic types to specific ones
|
||||
const expandedTypes = allowedTypes.flatMap((type) => {
|
||||
if (type === 'image/*') {
|
||||
return [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/gif',
|
||||
];
|
||||
return ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'];
|
||||
}
|
||||
if (type === 'video/*') {
|
||||
return [
|
||||
'video/mp4',
|
||||
'video/mpeg',
|
||||
];
|
||||
return ['video/mp4', 'video/mpeg'];
|
||||
}
|
||||
return [type];
|
||||
});
|
||||
|
|
@ -214,12 +206,11 @@ export function useUppyUploader(props: {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log(result);
|
||||
if (transloadit.length > 0) {
|
||||
// @ts-ignore
|
||||
const allRes = result.transloadit[0].results;
|
||||
const toSave = uniq<string>(
|
||||
allRes[Object.keys(allRes)[0]].flatMap((item: any) =>
|
||||
(allRes[Object.keys(allRes)[0]] || []).flatMap((item: any) =>
|
||||
item.url.split('/').pop()
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import { MarketplaceProvider } from '@gitroom/frontend/components/marketplace/ma
|
|||
import { SpecialMessage } from '@gitroom/frontend/components/marketplace/special.message';
|
||||
import { usePageVisibility } from '@gitroom/react/helpers/use.is.visible';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
export const Message: FC<{
|
||||
message: Message;
|
||||
seller: SellerBuyer;
|
||||
|
|
@ -89,7 +90,7 @@ export const Message: FC<{
|
|||
);
|
||||
}, [amITheBuyerOrSeller, message]);
|
||||
const time = useMemo(() => {
|
||||
return dayjs(message.createdAt).format('h:mm A');
|
||||
return newDayjs(message.createdAt).format('h:mm A');
|
||||
}, [message]);
|
||||
return (
|
||||
<div className="flex gap-[10px]">
|
||||
|
|
|
|||
51
apps/frontend/src/components/new-launch/a.component.tsx
Normal file
51
apps/frontend/src/components/new-launch/a.component.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback } from 'react';
|
||||
|
||||
export const AComponent: FC<{
|
||||
editor: any;
|
||||
currentValue: string;
|
||||
}> = ({ editor }) => {
|
||||
const mark = () => {
|
||||
const previousUrl = editor?.getAttributes('link')?.href;
|
||||
const url = window.prompt('URL', previousUrl);
|
||||
|
||||
// cancelled
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor?.chain()?.focus()?.extendMarkRange('link')?.unsetLink()?.run();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// update link
|
||||
try {
|
||||
editor?.chain()?.focus()?.extendMarkRange('link')?.setLink({ href: url })?.run();
|
||||
} catch (e) {
|
||||
}
|
||||
editor?.commands?.focus();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
onClick={mark}
|
||||
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 26 26"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.7079 8.29252C17.8008 8.38539 17.8746 8.49568 17.9249 8.61708C17.9752 8.73847 18.0011 8.8686 18.0011 9.00002C18.0011 9.13143 17.9752 9.26156 17.9249 9.38296C17.8746 9.50436 17.8008 9.61465 17.7079 9.70752L9.70786 17.7075C9.61495 17.8004 9.50465 17.8741 9.38325 17.9244C9.26186 17.9747 9.13175 18.0006 9.00036 18.0006C8.86896 18.0006 8.73885 17.9747 8.61746 17.9244C8.49607 17.8741 8.38577 17.8004 8.29286 17.7075C8.19995 17.6146 8.12625 17.5043 8.07596 17.3829C8.02568 17.2615 7.9998 17.1314 7.9998 17C7.9998 16.8686 8.02568 16.7385 8.07596 16.6171C8.12625 16.4957 8.19995 16.3854 8.29286 16.2925L16.2929 8.29252C16.3857 8.19954 16.496 8.12578 16.6174 8.07546C16.7388 8.02513 16.8689 7.99923 17.0004 7.99923C17.1318 7.99923 17.2619 8.02513 17.3833 8.07546C17.5047 8.12578 17.615 8.19954 17.7079 8.29252ZM23.9504 2.05002C23.3003 1.39993 22.5286 0.884251 21.6793 0.532423C20.83 0.180596 19.9197 -0.000488281 19.0004 -0.000488281C18.081 -0.000488281 17.1707 0.180596 16.3214 0.532423C15.4721 0.884251 14.7004 1.39993 14.0504 2.05002L10.2929 5.80627C10.1052 5.99391 9.9998 6.2484 9.9998 6.51377C9.9998 6.77913 10.1052 7.03363 10.2929 7.22127C10.4805 7.40891 10.735 7.51432 11.0004 7.51432C11.2657 7.51432 11.5202 7.40891 11.7079 7.22127L15.4654 3.47127C16.4065 2.55083 17.6726 2.03866 18.989 2.04591C20.3053 2.05316 21.5657 2.57924 22.4966 3.50999C23.4276 4.44074 23.9539 5.70105 23.9613 7.01742C23.9688 8.33379 23.4569 9.6 22.5366 10.5413L18.7779 14.2988C18.5902 14.4862 18.4847 14.7406 18.4846 15.0058C18.4845 15.2711 18.5898 15.5255 18.7772 15.7131C18.9647 15.9008 19.219 16.0063 19.4843 16.0064C19.7495 16.0065 20.004 15.9012 20.1916 15.7138L23.9504 11.95C24.6004 11.3 25.1161 10.5283 25.468 9.67897C25.8198 8.82964 26.0009 7.91933 26.0009 7.00002C26.0009 6.0807 25.8198 5.17039 25.468 4.32107C25.1161 3.47174 24.6004 2.70004 23.9504 2.05002ZM14.2929 18.7775L10.5354 22.535C10.073 23.0078 9.52136 23.3842 8.9125 23.6423C8.30365 23.9004 7.64963 24.0352 6.98832 24.0389C6.32702 24.0425 5.67156 23.9149 5.05989 23.6635C4.44823 23.4121 3.89252 23.0418 3.42494 22.5742C2.95736 22.1065 2.5872 21.5507 2.33589 20.939C2.08458 20.3273 1.95711 19.6718 1.96087 19.0105C1.96463 18.3492 2.09954 17.6952 2.35779 17.0864C2.61603 16.4776 2.99249 15.9261 3.46536 15.4638L7.22161 11.7075C7.40925 11.5199 7.51466 11.2654 7.51466 11C7.51466 10.7347 7.40925 10.4802 7.22161 10.2925C7.03397 10.1049 6.77947 9.99946 6.51411 9.99946C6.24874 9.99946 5.99425 10.1049 5.80661 10.2925L2.05036 14.05C0.737536 15.3628 0 17.1434 0 19C0 20.8566 0.737536 22.6372 2.05036 23.95C3.36318 25.2628 5.14375 26.0004 7.00036 26.0004C8.85697 26.0004 10.6375 25.2628 11.9504 23.95L15.7079 20.1913C15.8953 20.0036 16.0006 19.7492 16.0005 19.4839C16.0004 19.2187 15.8949 18.9644 15.7072 18.7769C15.5196 18.5894 15.2652 18.4842 14.9999 18.4843C14.7347 18.4844 14.4803 18.5899 14.2929 18.7775Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,6 +8,7 @@ import { ManageModal } from '@gitroom/frontend/components/new-launch/manage.moda
|
|||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
|
||||
export interface AddEditModalProps {
|
||||
dummy?: boolean;
|
||||
|
|
@ -46,7 +47,7 @@ export const AddEditModal: FC<AddEditModalProps> = (props) => {
|
|||
const integrations = useLaunchStore((state) => state.integrations);
|
||||
useEffect(() => {
|
||||
setDummy(!!props.dummy);
|
||||
setDate(props.date || dayjs());
|
||||
setDate(props.date || newDayjs());
|
||||
setAllIntegrations(props.allIntegrations || []);
|
||||
setIsCreateSet(!!props.addEditSets);
|
||||
}, []);
|
||||
|
|
@ -116,6 +117,7 @@ export const AddEditModalInnerInner: FC<AddEditModalProps> = (props) => {
|
|||
internal,
|
||||
setTags,
|
||||
setEditor,
|
||||
setRepeater,
|
||||
} = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
reset: state.reset,
|
||||
|
|
@ -126,11 +128,15 @@ export const AddEditModalInnerInner: FC<AddEditModalProps> = (props) => {
|
|||
internal: state.internal,
|
||||
setTags: state.setTags,
|
||||
setEditor: state.setEditor,
|
||||
setRepeater: state.setRepeater
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (existingData.integration) {
|
||||
if (existingData?.posts?.[0]?.intervalInDays) {
|
||||
setRepeater(existingData.posts[0].intervalInDays);
|
||||
}
|
||||
setTags(
|
||||
// @ts-ignore
|
||||
existingData?.posts?.[0]?.tags?.map((p: any) => ({
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ export const BoldText: FC<{
|
|||
currentValue: string;
|
||||
}> = ({ editor }) => {
|
||||
const mark = () => {
|
||||
editor.commands.unsetUnderline();
|
||||
editor.commands.toggleBold();
|
||||
editor.commands.focus();
|
||||
editor?.commands?.unsetUnderline();
|
||||
editor?.commands?.toggleBold();
|
||||
editor?.commands?.focus();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const Bullets: FC<{
|
|||
currentValue: string;
|
||||
}> = ({ editor }) => {
|
||||
const bullet = () => {
|
||||
editor.commands.toggleBulletList();
|
||||
editor?.commands?.toggleBulletList();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import React, { FC } from 'react';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import React, {
|
|||
ClipboardEvent,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
Fragment,
|
||||
} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
|
|
@ -20,7 +21,10 @@ import { BoldText } from '@gitroom/frontend/components/new-launch/bold.text';
|
|||
import { UText } from '@gitroom/frontend/components/new-launch/u.text';
|
||||
import { SignatureBox } from '@gitroom/frontend/components/signature';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import {
|
||||
SelectedIntegrations,
|
||||
useLaunchStore,
|
||||
} from '@gitroom/frontend/components/new-launch/store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { AddPostButton } from '@gitroom/frontend/components/new-launch/add.post.button';
|
||||
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
|
|
@ -28,10 +32,10 @@ import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow
|
|||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
|
||||
import { LinkedinCompanyPop } from '@gitroom/frontend/components/launches/helpers/linkedin.component';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
|
||||
import { Dashboard } from '@uppy/react';
|
||||
import Link from '@tiptap/extension-link';
|
||||
import {
|
||||
useEditor,
|
||||
EditorContent,
|
||||
|
|
@ -50,6 +54,11 @@ import { BulletList, ListItem } from '@tiptap/extension-list';
|
|||
import { Bullets } from '@gitroom/frontend/components/new-launch/bullets.component';
|
||||
import Heading from '@tiptap/extension-heading';
|
||||
import { HeadingComponent } from '@gitroom/frontend/components/new-launch/heading.component';
|
||||
import Mention from '@tiptap/extension-mention';
|
||||
import { suggestion } from '@gitroom/frontend/components/new-launch/mention.component';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { AComponent } from '@gitroom/frontend/components/new-launch/a.component';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
const InterceptBoldShortcut = Extension.create({
|
||||
name: 'preventBoldWithUnderline',
|
||||
|
|
@ -58,8 +67,8 @@ const InterceptBoldShortcut = Extension.create({
|
|||
return {
|
||||
'Mod-b': () => {
|
||||
// For example, toggle bold while removing underline
|
||||
this.editor.commands.unsetUnderline();
|
||||
return this.editor.commands.toggleBold();
|
||||
this?.editor?.commands?.unsetUnderline();
|
||||
return this?.editor?.commands?.toggleBold();
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
@ -72,58 +81,13 @@ const InterceptUnderlineShortcut = Extension.create({
|
|||
return {
|
||||
'Mod-u': () => {
|
||||
// For example, toggle bold while removing underline
|
||||
this.editor.commands.unsetBold();
|
||||
return this.editor.commands.toggleUnderline();
|
||||
this?.editor?.commands?.unsetBold();
|
||||
return this?.editor?.commands?.toggleUnderline();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const Span = Node.create({
|
||||
name: 'mention',
|
||||
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
selectable: false,
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
linkedinId: {
|
||||
default: null,
|
||||
},
|
||||
label: {
|
||||
default: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'span[data-linkedin-id]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
'span',
|
||||
mergeAttributes(
|
||||
// Exclude linkedinId from HTMLAttributes to avoid duplication
|
||||
Object.fromEntries(
|
||||
Object.entries(HTMLAttributes).filter(([key]) => key !== 'linkedinId')
|
||||
),
|
||||
{
|
||||
'data-linkedin-id': HTMLAttributes.linkedinId,
|
||||
class: 'mention',
|
||||
}
|
||||
),
|
||||
`@${HTMLAttributes.label}`,
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
export const EditorWrapper: FC<{
|
||||
totalPosts: number;
|
||||
value: string;
|
||||
|
|
@ -155,6 +119,8 @@ export const EditorWrapper: FC<{
|
|||
editor,
|
||||
loadedState,
|
||||
setLoadedState,
|
||||
selectedIntegration,
|
||||
chars,
|
||||
} = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
internal: state.internal.find((p) => p.integration.id === state.current),
|
||||
|
|
@ -183,6 +149,8 @@ export const EditorWrapper: FC<{
|
|||
editor: state.editor,
|
||||
loadedState: state.loaded,
|
||||
setLoadedState: state.setLoaded,
|
||||
selectedIntegration: state.selectedIntegrations,
|
||||
chars: state.chars,
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
@ -398,6 +366,21 @@ export const EditorWrapper: FC<{
|
|||
totalChars={totalChars}
|
||||
appendImages={appendImages(index)}
|
||||
dummy={dummy}
|
||||
selectedIntegration={selectedIntegration}
|
||||
chars={chars}
|
||||
childButton={
|
||||
<>
|
||||
{canEdit ? (
|
||||
<AddPostButton
|
||||
num={index}
|
||||
onClick={addValue(index)}
|
||||
postComment={postComment}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-[25px]" />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-[10px]">
|
||||
|
|
@ -447,16 +430,6 @@ export const EditorWrapper: FC<{
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canEdit ? (
|
||||
<AddPostButton
|
||||
num={index}
|
||||
onClick={addValue(index)}
|
||||
postComment={postComment}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-[25px]" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -477,7 +450,10 @@ export const Editor: FC<{
|
|||
validateChars?: boolean;
|
||||
identifier?: string;
|
||||
totalChars?: number;
|
||||
selectedIntegration: SelectedIntegrations[];
|
||||
dummy: boolean;
|
||||
chars: Record<string, number>;
|
||||
childButton?: React.ReactNode;
|
||||
}> = (props) => {
|
||||
const {
|
||||
editorType = 'normal',
|
||||
|
|
@ -485,11 +461,13 @@ export const Editor: FC<{
|
|||
pictures,
|
||||
setImages,
|
||||
num,
|
||||
autoComplete,
|
||||
validateChars,
|
||||
identifier,
|
||||
appendImages,
|
||||
selectedIntegration,
|
||||
dummy,
|
||||
chars,
|
||||
childButton,
|
||||
} = props;
|
||||
const user = useUser();
|
||||
const [id] = useState(makeId(10));
|
||||
|
|
@ -544,31 +522,14 @@ export const Editor: FC<{
|
|||
|
||||
const addText = useCallback(
|
||||
(emoji: string) => {
|
||||
editorRef?.current?.editor.commands.insertContent(emoji);
|
||||
editorRef?.current?.editor.commands.focus();
|
||||
editorRef?.current?.editor?.commands?.insertContent(emoji);
|
||||
editorRef?.current?.editor?.commands?.focus();
|
||||
},
|
||||
[props.value, id]
|
||||
);
|
||||
|
||||
const addLinkedinTag = useCallback((text: string) => {
|
||||
const id = text.split('(')[1].split(')')[0];
|
||||
const name = text.split('[')[1].split(']')[0];
|
||||
|
||||
editorRef?.current?.editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContent({
|
||||
type: 'mention',
|
||||
attrs: {
|
||||
linkedinId: id,
|
||||
label: name,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div className="relative bg-bigStrip" id={id}>
|
||||
<div className="flex gap-[5px] bg-newBgLineColor border-b border-t border-customColor3 justify-center items-center p-[5px]">
|
||||
<SignatureBox editor={editorRef?.current?.editor} />
|
||||
|
|
@ -580,27 +541,29 @@ export const Editor: FC<{
|
|||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
{(editorType === 'markdown' || editorType === 'html') && (
|
||||
<>
|
||||
<Bullets
|
||||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
<HeadingComponent
|
||||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(editorType === 'markdown' || editorType === 'html') &&
|
||||
identifier !== 'telegram' && (
|
||||
<>
|
||||
<AComponent
|
||||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
<Bullets
|
||||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
<HeadingComponent
|
||||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
|
||||
onClick={() => setEmojiPickerOpen(!emojiPickerOpen)}
|
||||
>
|
||||
{'\uD83D\uDE00'}
|
||||
</div>
|
||||
{identifier === 'linkedin' || identifier === 'linkedin-page' ? (
|
||||
<LinkedinCompanyPop addText={addLinkedinTag} />
|
||||
) : null}
|
||||
<div className="relative">
|
||||
<div className="absolute z-[200] top-[35px] -start-[50px]">
|
||||
<EmojiPicker
|
||||
|
|
@ -692,17 +655,48 @@ export const Editor: FC<{
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-10px end-[25px]">
|
||||
{(props?.totalChars || 0) > 0 && (
|
||||
<div
|
||||
className={clsx(
|
||||
'text-end text-sm mt-1',
|
||||
valueWithoutHtml.length > props.totalChars && '!text-red-500'
|
||||
)}
|
||||
>
|
||||
{valueWithoutHtml.length}/{props.totalChars}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex-1">{childButton}</div>
|
||||
<div className="bottom-10px end-[25px]">
|
||||
{(props?.totalChars || 0) > 0 ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'text-end text-sm mt-1',
|
||||
valueWithoutHtml.length > props.totalChars && '!text-red-500'
|
||||
)}
|
||||
>
|
||||
{valueWithoutHtml.length}/{props.totalChars}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
'text-end text-sm mt-1 grid grid-cols-[max-content_max-content] gap-x-[5px]'
|
||||
)}
|
||||
>
|
||||
{selectedIntegration?.map((p) => (
|
||||
<Fragment key={p.integration.id}>
|
||||
<div
|
||||
className={
|
||||
valueWithoutHtml.length > chars?.[p.integration.id] &&
|
||||
'!text-red-500'
|
||||
}
|
||||
>
|
||||
{p.integration.name} ({capitalize(p.integration.identifier)}
|
||||
):
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
valueWithoutHtml.length > chars?.[p.integration.id] &&
|
||||
'!text-red-500'
|
||||
}
|
||||
>
|
||||
{valueWithoutHtml.length}/{chars?.[p.integration.id]}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -717,6 +711,43 @@ export const OnlyEditor = forwardRef<
|
|||
paste?: (event: ClipboardEvent | File[]) => void;
|
||||
}
|
||||
>(({ editorType, value, onChange, paste }, ref) => {
|
||||
const fetch = useFetch();
|
||||
const { internal } = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
internal: state.internal.find((p) => p.integration.id === state.current),
|
||||
}))
|
||||
);
|
||||
|
||||
const loadList = useCallback(
|
||||
async (query: string) => {
|
||||
if (query.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!internal?.integration.id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const load = await fetch('/integrations/mentions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'mention',
|
||||
id: internal.integration.id,
|
||||
data: { query },
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await load.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error loading mentions:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[internal, fetch]
|
||||
);
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
Document,
|
||||
|
|
@ -726,12 +757,113 @@ export const OnlyEditor = forwardRef<
|
|||
Bold,
|
||||
InterceptBoldShortcut,
|
||||
InterceptUnderlineShortcut,
|
||||
Span,
|
||||
BulletList,
|
||||
ListItem,
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3],
|
||||
}),
|
||||
...(editorType === 'html' || editorType === 'markdown'
|
||||
? [
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
autolink: true,
|
||||
defaultProtocol: 'https',
|
||||
protocols: ['http', 'https'],
|
||||
isAllowedUri: (url, ctx) => {
|
||||
try {
|
||||
// prevent transforming plain emails like foo@bar.com into links
|
||||
const trimmed = String(url).trim();
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (emailPattern.test(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// construct URL
|
||||
const parsedUrl = url.includes(':')
|
||||
? new URL(url)
|
||||
: new URL(`${ctx.defaultProtocol}://${url}`);
|
||||
|
||||
// use default validation
|
||||
if (!ctx.defaultValidate(parsedUrl.href)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallowed protocols
|
||||
const disallowedProtocols = ['ftp', 'file', 'mailto'];
|
||||
const protocol = parsedUrl.protocol.replace(':', '');
|
||||
|
||||
if (disallowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only allow protocols specified in ctx.protocols
|
||||
const allowedProtocols = ctx.protocols.map((p) =>
|
||||
typeof p === 'string' ? p : p.scheme
|
||||
);
|
||||
|
||||
if (!allowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// all checks have passed
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
shouldAutoLink: (url) => {
|
||||
try {
|
||||
// prevent auto-linking of plain emails like foo@bar.com
|
||||
const trimmed = String(url).trim();
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (emailPattern.test(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// construct URL
|
||||
const parsedUrl = url.includes(':')
|
||||
? new URL(url)
|
||||
: new URL(`https://${url}`);
|
||||
|
||||
// only auto-link if the domain is not in the disallowed list
|
||||
const disallowedDomains = [
|
||||
'example-no-autolink.com',
|
||||
'another-no-autolink.com',
|
||||
];
|
||||
const domain = parsedUrl.hostname;
|
||||
|
||||
return !disallowedDomains.includes(domain);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(internal?.integration?.id
|
||||
? [
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
renderHTML({ options, node }) {
|
||||
return [
|
||||
'span',
|
||||
mergeAttributes(options.HTMLAttributes, {
|
||||
'data-mention-id': node.attrs.id || '',
|
||||
'data-mention-label': node.attrs.label || '',
|
||||
}),
|
||||
`@${node.attrs.label}`,
|
||||
];
|
||||
},
|
||||
suggestion: suggestion(loadList),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(editorType === 'html' || editorType === 'markdown'
|
||||
? [
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
History.configure({
|
||||
depth: 100, // default is 100
|
||||
newGroupDelay: 100, // default is 500ms
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ export const ThreadFinisher = () => {
|
|||
<div className="flex gap-[4px]">
|
||||
<div className="flex-1 editor text-textColor">
|
||||
<Editor
|
||||
chars={{}}
|
||||
selectedIntegration={[]}
|
||||
onChange={(val) => setValue('thread_finisher', val)}
|
||||
value={value}
|
||||
totalPosts={1}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue