diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..3ba1471 --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,213 @@ +name: CICD + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + Build: + name: Build + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js 18.x + uses: actions/setup-node@v2 + with: + node-version: "18.x" + + - name: Install dependencies + run: | + npm install -g pnpm && pnpm install --no-frozen-lockfile + + - name: Setup environment variables + run: echo "${{ secrets.APPS_WEB_ENV }}" >> apps/web/.env + + - name: Check environment variables + run: | + cat apps/web/.env + + - name: Build + run: | + pnpm build + + - name: Check build + run: | + ls + + # - name: Test + # run: | + # pnpm test:ci --if-present + + Release: + name: Release + needs: [Build, DeployStagingWeb, DeployStagingDocs] + permissions: + contents: write + issues: write + pull-requests: write + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: ./.github/workflows/release.yml + + Check: + name: Check PR + permissions: + contents: write + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js 18.x + uses: actions/setup-node@v2 + with: + node-version: "18.x" + + - name: Install dependencies + run: | + npm install -g pnpm && pnpm install --no-frozen-lockfile + + - name: Setup environment variables + run: echo "${{ secrets.APPS_WEB_ENV }}" >> apps/web/.env + + - name: Check environment variables + run: | + cat apps/web/.env + + # - name: Test + # run: | + # pnpm test --if-present + + DeployStagingWeb: + name: Deploy to Staging Web + permissions: + contents: write + if: github.event.ref == 'refs/heads/main' + needs: [Build] + runs-on: ubuntu-latest + environment: + name: Production Web + url: "http://52.54.237.87:3000" + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js 18.x + uses: actions/setup-node@v2 + with: + node-version: "18.x" + + - name: Move to docs directory + run: cd apps/web + + - name: Check current directory + run: | + pwd + + - name: Install dependencies + run: | + npm install -g pnpm + cd apps/web && pnpm install --no-frozen-lockfile + + - name: Setup environment variables + run: echo "${{ secrets.APPS_WEB_ENV }}" >> apps/web/.env + + - name: Check environment variables + run: | + cat apps/web/.env + + - name: Build + run: | + cd apps/web && pnpm build + + - name: Stop server + run: | + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" ssh -o StrictHostKeyChecking=no ubuntu@52.54.237.87 "cd apps/web && pm2 stop all" + + - name: Clean LightSail directory + run: | + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" ssh -o StrictHostKeyChecking=no ubuntu@52.54.237.87 "rm -rf apps/web" + + - name: Deploy to Lightsail using scp + run: | + sudo apt-get install -y sshpass + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" scp -o StrictHostKeyChecking=no -r apps/web ubuntu@52.54.237.87:/home/ubuntu/apps --exclude node_modules + + - name: Set custom package.json + run: | + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" ssh -o StrictHostKeyChecking=no ubuntu@52.54.237.87 "cd apps/web && echo ${{secrets.APPS_WEB_PACKAGE}} > package.json" + + - name: Start server + run: | + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" ssh -o StrictHostKeyChecking=no ubuntu@52.54.237.87 "cd apps/web && pnpm install && pm2 start pnpm --name "web" -- start" + + - name: Create output artifact + uses: actions/upload-artifact@v2 + with: + name: web + path: apps/web + + DeployStagingDocs: + name: Deploy to Staging Docs + permissions: + contents: write + if: github.event.ref == 'refs/heads/main' + needs: [Build] + runs-on: ubuntu-latest + environment: + name: Production Docs + url: "http://52.54.237.87:3001" + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js 18.x + uses: actions/setup-node@v2 + with: + node-version: "18.x" + + - name: Move to docs directory + run: cd apps/docs + + - name: Check current directory + run: | + pwd + + - name: Install dependencies + run: | + npm install -g pnpm + cd apps/web && pnpm install --no-frozen-lockfile + + - name: Build + run: | + cd apps/docs && pnpm build + + - name: Stop server + run: | + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" ssh -o StrictHostKeyChecking=no ubuntu@52.54.237.87 "cd apps/docs && pm2 stop all" + + - name: Clean LightSail directory + run: | + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" ssh -o StrictHostKeyChecking=no ubuntu@52.54.237.87 "rm -rf apps/docs" + + - name: Deploy to Lightsail using scp excluding node_modules + run: | + sudo apt-get install -y sshpass + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" scp -o StrictHostKeyChecking=no -r apps/docs ubuntu@52.54.237.87:/home/ubuntu/apps --exclude node_modules + + - name: Set custom package.json + run: | + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" ssh -o StrictHostKeyChecking=no ubuntu@52.54.237.87 "cd apps/docs && echo ${{secrets.APPS_DOCS_PACKAGE}} > package.json" + + - name: Start server + run: | + sshpass -p "${{ secrets.LIGHTSAIL_SSH_PASS }}" ssh -o StrictHostKeyChecking=no ubuntu@52.54.237.87 "cd apps/docs && pnpm install && pm2 start pnpm --name "docs" -- start" + + - name: Create output artifact + uses: actions/upload-artifact@v2 + with: + name: docs + path: apps/docs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cf29b4..fce015f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,30 +1,24 @@ name: Release on: - workflow_dispatch: + workflow_call: jobs: release: + name: Release permissions: contents: write issues: write pull-requests: write runs-on: ubuntu-latest steps: - - name: Checkout - uses: SpicyPizza/create-envfile@v1.3 - with: - envkey_DEBUG: false - envkey_PORT: 3000 - envkey_DATABASE_URL: ${{ secrets.DATABASE_URL }} - directory: ./apps/web - file_name: .env + - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: 18 - - run: npm i -g pnpm - - run: pnpm install --no-frozen-lockfile - - run: pnpm build + node-version: "18.x" + - run: npm -g install @semantic-release/git semantic-release && semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/docs/pages/_meta.json b/apps/docs/pages/_meta.json index 164927d..06e8841 100644 --- a/apps/docs/pages/_meta.json +++ b/apps/docs/pages/_meta.json @@ -1,7 +1,7 @@ { "index": "Introduction", - "another": "Another Page", - "advanced": "Advanced (A Folder)", + "builds": "Build", + "setup":"Setup", "about": { "title": "About", "type": "page" @@ -11,5 +11,7 @@ "type": "page", "href": "https://twitter.com/shuding_", "newWindow": true - } + }, + "frontend": "Front-End", + "backend": "Back-End" } diff --git a/apps/docs/pages/advanced.mdx b/apps/docs/pages/advanced.mdx deleted file mode 100644 index a1a5148..0000000 --- a/apps/docs/pages/advanced.mdx +++ /dev/null @@ -1,3 +0,0 @@ -# Advanced - -This is the index page for the Advanced folder! diff --git a/apps/docs/pages/advanced/satori.mdx b/apps/docs/pages/advanced/satori.mdx deleted file mode 100644 index 46eb19f..0000000 --- a/apps/docs/pages/advanced/satori.mdx +++ /dev/null @@ -1,3 +0,0 @@ -# Satori - -Satori (悟り) is a Japanese Buddhist term for awakening, "comprehension; understanding". diff --git a/apps/docs/pages/another.mdx b/apps/docs/pages/another.mdx deleted file mode 100644 index 7fb6a02..0000000 --- a/apps/docs/pages/another.mdx +++ /dev/null @@ -1,31 +0,0 @@ -# Another Page - -```js filename="demo.js" {3} copy -let a = 1; - -console.log(a); -``` - -## Component - -import { useState } from 'react' - -{/* Import CSS modules */} -import styles from '../components/counters.module.css' - -export const Counter = () => { - const [count, setCount] = useState(0); - return ( -
- -
- ); -}; - - - -## External Component - -import Counters from '../components/counters' - - diff --git a/apps/docs/pages/builds/_meta.json b/apps/docs/pages/builds/_meta.json new file mode 100644 index 0000000..66c9f6d --- /dev/null +++ b/apps/docs/pages/builds/_meta.json @@ -0,0 +1,3 @@ +{ + "environment": "Environment Api Keys" +} \ No newline at end of file diff --git a/apps/docs/pages/builds/environment/_meta.json b/apps/docs/pages/builds/environment/_meta.json new file mode 100644 index 0000000..6a96a3f --- /dev/null +++ b/apps/docs/pages/builds/environment/_meta.json @@ -0,0 +1,8 @@ +{ + "database": "Database PostgreSQL", + "openai": "OpenAI Providers Connection", + "auth0": "Accounts Authentication Auth0", + "pricing": "Chat GPT and Dall-E Models Pricing", + "s3bucket": "S3 Bucket for image storage" + +} \ No newline at end of file diff --git a/apps/docs/pages/builds/environment/auth0.mdx b/apps/docs/pages/builds/environment/auth0.mdx new file mode 100644 index 0000000..9a9ee0f --- /dev/null +++ b/apps/docs/pages/builds/environment/auth0.mdx @@ -0,0 +1,11 @@ +# **Accounts Authentication Auth0** +> ```.env filename=".env" copy +> # Providers Connections +> AUTH0_SECRET= "" +> AUTH0_BASE_URL="http://localhost:3000" +> AUTH0_ISSUER_BASE_URL= "" +> AUTH0_CLIENT_ID= "" +> AUTH0_CLIENT_SECRET= "" +> ``` +> > To get the secret environment key for Auth0 +> > \ No newline at end of file diff --git a/apps/docs/pages/builds/environment/database.mdx b/apps/docs/pages/builds/environment/database.mdx new file mode 100644 index 0000000..76a35d3 --- /dev/null +++ b/apps/docs/pages/builds/environment/database.mdx @@ -0,0 +1,11 @@ +# **Database PostgreSQL** +> The WizePrompt database +> ```.env filename=".env" copy +> # DB PostgreSQL +> DATABASE_URL= " " +> ``` +> > To get the environment key for the web database you will need to migrate the **Prisma Squema** of your local repository to your *PostgreSQL* app. +> > ```js +> > npx prisma migrate +> > ``` +> > You will need to have **Prisma** already installed \ No newline at end of file diff --git a/apps/docs/pages/builds/environment/openai.mdx b/apps/docs/pages/builds/environment/openai.mdx new file mode 100644 index 0000000..5806d2a --- /dev/null +++ b/apps/docs/pages/builds/environment/openai.mdx @@ -0,0 +1,7 @@ +# **OpenAI Providers Connection** +> ```.env filename=".env" copy +> # Providers Connections +> OPENAI_API_KEY= "" +> ``` +> > To get the environment key +> > \ No newline at end of file diff --git a/apps/docs/pages/builds/environment/pricing.mdx b/apps/docs/pages/builds/environment/pricing.mdx new file mode 100644 index 0000000..e221bfc --- /dev/null +++ b/apps/docs/pages/builds/environment/pricing.mdx @@ -0,0 +1,16 @@ +# **Chat GPT and Dall-E models pricing** +> > *GPT Pricing* +> > ```.env filename=".env" copy +> > # GPT Model pricing per 1000 tokens +> > NEXT_PUBLIC_GPT_35_INPUT = "0.0015" +> > NEXT_PUBLIC_GPT_35_OUTPUT = "0.0020" +> > NEXT_PUBLIC_GPT_4_INPUT = "0.03" +> > NEXT_PUBLIC_GPT_4_OUTPUT = "0.06" +> > ``` +> > *Dall-E Pricing* +> > ```.env filename=".env" copy +> > # DALLE Model pricing per resolution +> > NEXT_PUBLIC_DALLE_256 = "0.016" +> > NEXT_PUBLIC_DALLE_512 = "0.018" +> > NEXT_PUBLIC_DALLE_1024 = "0.02" +> > ``` \ No newline at end of file diff --git a/apps/docs/pages/builds/environment/s3bucket.mdx b/apps/docs/pages/builds/environment/s3bucket.mdx new file mode 100644 index 0000000..761764f --- /dev/null +++ b/apps/docs/pages/builds/environment/s3bucket.mdx @@ -0,0 +1,7 @@ +# **S3 Bucket for image storage** +> ```.env filename=".env" copy +> # S3 Bucket +> S3_SECRET_KEY = "" +> ``` +> > To get the environment secret key for the S3 Bucket +> > \ No newline at end of file diff --git a/apps/docs/pages/especifications.mdx b/apps/docs/pages/especifications.mdx new file mode 100644 index 0000000..068b76b --- /dev/null +++ b/apps/docs/pages/especifications.mdx @@ -0,0 +1,11 @@ +# Project requirements and especifications + +## Requirements + +Tools and Hardware Requirements Declaration + +## Specifications + +Technical Specifications + + diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx index 058465c..8dad295 100644 --- a/apps/docs/pages/index.mdx +++ b/apps/docs/pages/index.mdx @@ -1,11 +1,27 @@ -# Introduction - -Welcome to Nextra! This is a basic docs template. You can use it as a starting point for your own project :) +# Welcome to WizePrompt -## What is Nextra? +## Project Overview +The web application WizePropmt is a software that permits the +communication between a user and different types of AI +language models. Through the use of credits, WizePrompt is able to +manage and regulate the user consults to the language models. -A **simple**, **powerful** and **flexible** site generation framework with everything you love from Next.js. +## Information of the Project +```js +App Name: WizePrompt +Customer: Wizeline -## Documentation +Project Administrator: +David Gerardo Martínez -The documentation is available at [https://nextra.site](https://nextra.site). +Development Team: +David Gerardo Martínez - William Frank Monroy +María Eugenia Ontiveros - Jackeline Conant +Emilio Ortiz - Rodrigo Rodriguez + +Design Team: +David Gerardo Martínez - William Frank Monroy +Jackeline Conant +``` + +** About this file diff --git a/apps/docs/pages/maintain.mdx b/apps/docs/pages/maintain.mdx new file mode 100644 index 0000000..1a1db1b --- /dev/null +++ b/apps/docs/pages/maintain.mdx @@ -0,0 +1 @@ +# Notes for future maintainers \ No newline at end of file diff --git a/apps/docs/pages/setup/_meta.json b/apps/docs/pages/setup/_meta.json new file mode 100644 index 0000000..3bf850e --- /dev/null +++ b/apps/docs/pages/setup/_meta.json @@ -0,0 +1,4 @@ +{ + "gitcode": "Git Code to Local System", + "packages": "Dependency Packages and Tools" +} \ No newline at end of file diff --git a/apps/docs/pages/setup/gitcode.mdx b/apps/docs/pages/setup/gitcode.mdx new file mode 100644 index 0000000..ffed24b --- /dev/null +++ b/apps/docs/pages/setup/gitcode.mdx @@ -0,0 +1,47 @@ +# Setup instructions + +## Fetching git code to local system + +Commands that will use Github tools to fetch WizePrompt git code to local system +* **Clone WizePrompt repository from Github web app:** +```js filename="TERMINAL" {1} copy +git clone https://github.com/wizelineacademy/itesm-socioformador-ago-dec-2023-team-02.git +``` +* **Check the main development branch:** +>```js filename="TERMINAL" {1} copy +>git checkout development +>``` +> **Fetch any changes made to the development branch:** +>```js filename="TERMINAL" {1} copy +>git fetch +>``` +> > If there are some changes to the branch write: +> > ```js filename="TERMINAL" {1} copy +> > git pull +> > ``` +>If previous commands were used the git code should be already updated and inside the local repository. + +## Installing project specific dependency packages and tools + +* **Pnpm package Manager** +> Pnpm is an efficient package manager for Wizepropmt dependencies installation. +> > **Pnpm installation** +> > +> *🞂 If you have pnpm installed already, skip this step and move to project dependencies.* +* **Project dependencies** +> Download and install the project dependencies for the app to run correctly with pnpm command. +>```js filename="TERMINAL" {1} copy +> pnpm install +>``` +> This specific command will appear, wait for it to end installing and test. +> > 🞂 For the other dependencies you will need to access the **web** and **docs** folder of your local repository and repeat the pnpm command. +> If all the dependencies are installed the system will respond with: +>```js +> Scope: all 6 workspace projects +>../.. prepare$ husky install +>│ husky - Git hooks installed +>└─ Done in X.Xs +>Done in XX.Xs +>``` + +## Commands / Instructions for starting a server / OS service required by the project to run etc \ No newline at end of file diff --git a/apps/docs/pages/setup/packages.mdx b/apps/docs/pages/setup/packages.mdx new file mode 100644 index 0000000..370e03d --- /dev/null +++ b/apps/docs/pages/setup/packages.mdx @@ -0,0 +1,22 @@ +## Installing project specific dependency packages and tools + +* **Pnpm package Manager** +> Pnpm is an efficient package manager for Wizepropmt dependencies installation. +> > **Pnpm installation** +> > +> *🞂 If you have pnpm installed already, skip this step and move to project dependencies.* +* **Project dependencies** +> Download and install the project dependencies for the app to run correctly with pnpm command. +>```js filename="TERMINAL" {1} copy +> pnpm install +>``` +> This specific command will appear, wait for it to end installing and test. + +> If all the dependencies are installed the system will respond with: +>```js +> Scope: all 6 workspace projects +>../.. prepare$ husky install +>│ husky - Git hooks installed +>└─ Done in X.Xs +>Done in XX.Xs +>``` \ No newline at end of file diff --git a/apps/docs/pages/testing.mdx b/apps/docs/pages/testing.mdx new file mode 100644 index 0000000..01e2f16 --- /dev/null +++ b/apps/docs/pages/testing.mdx @@ -0,0 +1 @@ +# Unit Testing or Basic testing steps \ No newline at end of file diff --git a/apps/docs/theme.config.tsx b/apps/docs/theme.config.tsx index 6b42fde..4fcf4b1 100644 --- a/apps/docs/theme.config.tsx +++ b/apps/docs/theme.config.tsx @@ -2,14 +2,11 @@ import React from "react"; import { DocsThemeConfig } from "nextra-theme-docs"; const config: DocsThemeConfig = { - logo: My Project, + logo: SAM WizePrompt Proyect, project: { - link: "https://github.com/shuding/nextra-docs-template", + link: "https://github.com/wizelineacademy/itesm-socioformador-ago-dec-2023-team-02", }, - chat: { - link: "https://discord.com", - }, - docsRepositoryBase: "https://github.com/shuding/nextra-docs-template", + docsRepositoryBase: "https://github.com/wizelineacademy/itesm-socioformador-ago-dec-2023-team-02", footer: { text: "Nextra Docs Template", }, diff --git a/apps/web/.env.example b/apps/web/.env.example index 07c7086..6412b9c 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -9,4 +9,7 @@ AUTH0_SECRET= AUTH0_BASE_URL= AUTH0_ISSUER_BASE_URL= AUTH0_CLIENT_ID= -AUTH0_CLIENT_SECRET= \ No newline at end of file +AUTH0_CLIENT_SECRET= + +# S3 Bucket +S3_SECRET_KEY= \ No newline at end of file diff --git a/apps/web/app/(root)/(admin)/layout.tsx b/apps/web/app/(root)/(admin)/layout.tsx deleted file mode 100644 index 8e93484..0000000 --- a/apps/web/app/(root)/(admin)/layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ - -export default function AdminRootLayout({ - children, -}: { - children: React.ReactNode; -}): any { - return ( - - - {children} - - - ); -} - \ No newline at end of file diff --git a/apps/web/app/(root)/(user)/conversation/[id]/page.tsx b/apps/web/app/(root)/(user)/conversation/[id]/page.tsx index e69de29..c654878 100644 --- a/apps/web/app/(root)/(user)/conversation/[id]/page.tsx +++ b/apps/web/app/(root)/(user)/conversation/[id]/page.tsx @@ -0,0 +1,50 @@ +import ConversationBody from "@/components/user/conversationBody/molecules/conversation-body"; + +/* +import { getConversationById } from "@/lib/conversation"; +import { getAllMessages } from "@/lib/message"; +import { Conversation, Message as WizepromptMessage } from "@prisma/client"; +import { convertToGptMessage } from "@/lib/helper/gpt/convert-message-type"; +import { Message } from "ai"; +import ConversationHeader from "@/components/molecules/user/conversationHeader/conversation-top-header"; +import PromptTextInput from "@/components/molecules/user/conversationBody/prompt-text-input"; +import MessageList from "@/components/molecules/user/conversationBody/message-list"; +import { JsonObject, JsonValue } from "@prisma/client/runtime/library"; + +const providerImage = + "https://avatars.githubusercontent.com/u/86160567?s=200&v=4"; // URL de la imagen del remitente + +const userImage = + "https://ui-avatars.com/api/?background=007CFF&color=fff&name=David"; + +interface ConversationParameters { + userContext: String; + responseContext: String; + temperature: Number; +} +*/ +export default function ConversationPage(): JSX.Element { + /* + //get conversation data from api + const conversationData = await getConversationById(id); + + //get messages of conversation + const messagesData = await getAllMessages(id); + + //validate data + + + //convert messages to gpt messages + const wizePromptMessages: WizepromptMessage[] = messagesData.data || []; + const messages: Message[] = wizePromptMessages.map(convertToGptMessage); + + //set parameters + const conversation = conversationData.data || {} as Conversation; + const parameters = conversation.parameters || {userContext: "", responseContext: "", temperature: 0.5} as ConversationParameters; + //const userContextParameters = parameters.userContext || ""; + */ + + return ( + + ); +} \ No newline at end of file diff --git a/apps/web/app/(root)/(user)/conversation/layout.tsx b/apps/web/app/(root)/(user)/conversation/layout.tsx index bd46cc6..d092688 100644 --- a/apps/web/app/(root)/(user)/conversation/layout.tsx +++ b/apps/web/app/(root)/(user)/conversation/layout.tsx @@ -1,23 +1,111 @@ -import "../../..globals.css"; import type { Metadata } from "next"; -import LeftSidebar from "@/components/shared/left-sidebar"; +import type { Tag, User } from "@prisma/client"; +import { Suspense } from "react"; +import { getSession } from "@auth0/nextjs-auth0"; +import { redirect } from "next/navigation"; +import ConversationSidebar from "@/components/user/conversationSidebar/organisms/conversation-sidebar"; +import { getAllConversationsByUserId } from "@/lib/conversation"; +import { getAllSidebarTagsByUserID } from "@/lib/tag"; +import type { SidebarConversation } from "@/types/sidebar-conversation-types"; +import { getAllModelsWithProvider } from "@/lib/model"; +import type { ModelWithProvider } from "@/types/moder-with-provider-types"; +import { PrismaUserContextProvider } from "@/context/prisma-user-context"; +import { getUserbyAuthID } from "@/lib/user"; +import { Skeleton } from "@/components/ui/skeleton"; +import { ConversationsContextProvider } from "@/context/conversations-context"; export const metadata: Metadata = { title: "WizePrompt", description: "", }; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}): any { +function SidebarSuspense(): JSX.Element { return ( -
- -
-
{children}
-
-
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+ +
+ +
+
+
+ ); +} + +export default async function ConversationRootLayout({children}: {children: React.ReactNode}): Promise { + const { user } = (await getSession()) || {}; + + //If no user, redirect to login + if (!user) { + redirect("/api/auth/login"); + } + + //get user from database + const userAuthID: string = user.sub; + const prismaUser: User | undefined = (await getUserbyAuthID(userAuthID)).data; + + // If fetching of user data failed, redirect to login. + if (!prismaUser){ + redirect("/api/auth/login"); + } + + const userConversations: SidebarConversation[] = prismaUser + ? (await getAllConversationsByUserId(prismaUser.id)).data || [] + : []; + const userTags: Tag[] = prismaUser + ? (await getAllSidebarTagsByUserID(prismaUser.id)).data || [] + : []; + const models: ModelWithProvider[] = prismaUser + ? (await getAllModelsWithProvider()).data || [] + : []; + + return ( + }> + + +
+ +
+ {children} +
+
+
+
+
); } diff --git a/apps/web/app/(root)/(user)/conversation/new/page.tsx b/apps/web/app/(root)/(user)/conversation/new/page.tsx index a21d535..6b96263 100644 --- a/apps/web/app/(root)/(user)/conversation/new/page.tsx +++ b/apps/web/app/(root)/(user)/conversation/new/page.tsx @@ -1,15 +1,10 @@ -import ChatComponent from "@/components/chat-component"; -// Prisma does not support Edge without the Data Proxy currently -// export const runtime = 'edge' -export const preferredRegion = "home"; -export const dynamic = "force-dynamic"; +import Welcome from "@/components/user/newConversation/organism/welcome"; +import { getAllModelsWithProvider } from "@/lib/model"; +import type { ModelWithProvider } from "@/types/moder-with-provider-types"; -function HomePage(): any { - return ( -
- -
- ); -} +export default async function NewConversationPage(): Promise { + const models: ModelWithProvider[] = + (await getAllModelsWithProvider()).data || []; -export default HomePage; + return ; +} diff --git a/apps/web/app/(root)/(user)/layout.tsx b/apps/web/app/(root)/(user)/layout.tsx index 3a92ce5..be2e913 100644 --- a/apps/web/app/(root)/(user)/layout.tsx +++ b/apps/web/app/(root)/(user)/layout.tsx @@ -1,11 +1,9 @@ -export default function UserRootLayout({ - children, -}: { - children: React.ReactNode; -}): any { - return ( - - {children} - - ); +// import { withPageAuthRequired } from "@auth0/nextjs-auth0"; + +// export default withPageAuthRequired(function User({ children }: any): any { +// return children; +// }); + +export default function UserLayout({ children }: any): any { + return children; } diff --git a/apps/web/app/(root)/admin/group/[id]/page.tsx b/apps/web/app/(root)/admin/group/[id]/page.tsx new file mode 100644 index 0000000..8e1c35b --- /dev/null +++ b/apps/web/app/(root)/admin/group/[id]/page.tsx @@ -0,0 +1,9 @@ +import GroupBody from "@/components/admin/groupBody/organisms/group-body"; + +export default function GroupPage(): JSX.Element { + return ( +
+ +
+ ); +} diff --git a/apps/web/app/(root)/admin/layout.tsx b/apps/web/app/(root)/admin/layout.tsx new file mode 100644 index 0000000..b274989 --- /dev/null +++ b/apps/web/app/(root)/admin/layout.tsx @@ -0,0 +1,43 @@ +import type { Group, User } from "@prisma/client"; +import { redirect } from "next/navigation"; +import { getSession } from "@auth0/nextjs-auth0"; +import GroupSidebar from "@/components/admin/groupSidebar/organisms/group-sidebar"; +import { GroupsContextProvider } from "@/context/groups-context"; +import { getAllGroups } from "@/lib/group"; +import { sortGroups } from "@/helpers/group-helpers"; +import { getUserbyAuthID } from "@/lib/user"; +import { PrismaUserContextProvider } from "@/context/prisma-user-context"; + +export default async function AdminRootLayout({children}: {children: React.ReactNode;}): Promise { + const { user } = (await getSession()) || {}; + + //If no user, redirect to login + if (!user) { + redirect("/api/auth/login"); + } + + //get user from database + const userAuthID: string = user.sub; + const prismaUser: User | undefined = (await getUserbyAuthID(userAuthID)).data; + + // If fetching of user data failed, redirect to login. + if (!prismaUser){ + redirect("/api/auth/login"); + } + + const allUsersGroupId = 1 + const initialGroups: Group[] = sortGroups((await getAllGroups()).data || [], allUsersGroupId) + + return ( + + +
+ +
+ {children} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/(root)/layout.tsx b/apps/web/app/(root)/layout.tsx index b2021af..7ebae68 100644 --- a/apps/web/app/(root)/layout.tsx +++ b/apps/web/app/(root)/layout.tsx @@ -28,7 +28,7 @@ export default function RootLayout({
{children}
- +
diff --git a/apps/web/app/(root)/page.tsx b/apps/web/app/(root)/page.tsx index b8e8639..daa2fea 100644 --- a/apps/web/app/(root)/page.tsx +++ b/apps/web/app/(root)/page.tsx @@ -1,12 +1,96 @@ -import ThemeButton from "../../components/theme-button"; -//Just for testing light and dark themes -export default function Home() { - return ( -
-
-

Home

- -
-
- ) -} \ No newline at end of file +import Image from "next/image"; +import { getSession } from "@auth0/nextjs-auth0"; +import { getUserbyAuthID, createUser } from "@/lib/user"; +import type { UserCreateData } from "@/types/user-types"; +import LoginButton from "@/components/login/login-button"; + +interface Parameters { + userContext: string; + responseContext: string; + temperature: number; + size: string; +} + +async function handleAuth0User(authUser: any): Promise { + // Replace Auth0SessionType with the actual type + + //check if current auth0 user exists in database + const authUserId: string = authUser.sub; + const result = await getUserbyAuthID(authUserId); + + //If user does not exists in database + if (result.status === 404) { + // User doesn't exist in database, create a new one + const newUser: UserCreateData = { + idAuth0: authUserId, + name: authUser.name, + email: authUser.email, + jobPosition: "", + role: "USER", + image: authUser.picture, + creditsRemaining: 0, + globalParameters: { + userContext: "", + responseContext: "", + temperature: 0.5, + size: "", + } as Parameters, + }; + + //create new user on the database + try { + void (await createUser(newUser, [1])); + } catch (error) { + console.log("error", error); + } + } +} + +export default async function Home(): Promise { + const { user } = (await getSession()) || {}; + + if (user) { + await handleAuth0User(user); + } + + return ( +
+ Wizeline Office +
+
+ Logo +
+
+
+

+ Welcome to  + + WizePrompt + +

+

+ The centralised platform for all your AI needs. +

+ +
+
+
+
+ ); +} diff --git a/apps/web/app/(root)/providers.tsx b/apps/web/app/(root)/providers.tsx index bb9fbc4..9aa06c5 100644 --- a/apps/web/app/(root)/providers.tsx +++ b/apps/web/app/(root)/providers.tsx @@ -11,7 +11,7 @@ import * as React from "react"; import { NextUIProvider } from "@nextui-org/react"; import { ThemeProvider as NextThemesProvider } from "next-themes"; import type { ThemeProviderProps } from "next-themes/dist/types"; -//import { UserProvider } from "@auth0/nextjs-auth0/client"; +import { UserProvider } from "@auth0/nextjs-auth0/client"; export interface ProvidersProps { children: JSX.Element | JSX.Element[]; @@ -23,12 +23,12 @@ export function Providers({ themeProps, }: ProvidersProps): JSX.Element { return ( - // + {children} - // + ); } diff --git a/apps/web/app/api/ai/openai/dalle/route.ts b/apps/web/app/api/ai/openai/dalle/route.ts new file mode 100644 index 0000000..5a8d18b --- /dev/null +++ b/apps/web/app/api/ai/openai/dalle/route.ts @@ -0,0 +1,62 @@ +// route.ts OpenAi route +import type { ResponseTypes } from "openai-edge"; +import { Configuration, OpenAIApi } from "openai-edge"; +import { NextResponse } from "next/server"; +import { uploadToLightsail } from "@/lib/helper/storage/upload-image"; + +export const runtime = "edge"; + +// Stores the API key of our OpenAI model +const config = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const openai = new OpenAIApi(config); + +/** + * Handles POST requests to the dalle model route. + * @param request - The incoming request object. + * @returns A base64 string or a NextResponse object. + */ +export async function POST(request: Request): Promise { + // Destructure the incoming request to get the messages array and image size + const { + messages, //previous messages of chat + size, // image size + } = await request.json(); // {messages:[], size: '256x256'} + + // Destructure the incoming request to get the messages array, model, and temperature + try { + /** + * Function that uses the "createImage" tool. + * Sets the prompt to the last message sent. + * Sets response format to a base64. + * Sets image size. + */ + const response: Response = await openai.createImage({ + prompt: messages[messages.length - 1].content, + response_format: "b64_json", + size, + }); + + const data = (await response.json()) as ResponseTypes["createImage"]; + + // Extracts the image string from the response data. + const img: string | undefined = data.data[0].b64_json; + + // Checks that the base64 image is not undefined and uploads the image to the lightsail bucket + // Returns the image url in markdown format + // Otherwise returns an error message + if (img) { + const imageUrl: string = await uploadToLightsail(img); + return new NextResponse(`![Generated image](${imageUrl})`, { + status: 200, + }); + } + return new NextResponse(`Error: No image was generated`, { status: 500 }); + } catch (error: any) { + // If an error occurs, log it to the console and send a message to the user + // console.error(error); + return new NextResponse(`Error: ${error.message}`, { status: 500 }); + } +} diff --git a/apps/web/app/api/ai/openai/gpt-3.5-turbo/route.ts b/apps/web/app/api/ai/openai/gpt-3.5-turbo/route.ts new file mode 100644 index 0000000..53e980d --- /dev/null +++ b/apps/web/app/api/ai/openai/gpt-3.5-turbo/route.ts @@ -0,0 +1,58 @@ +// route.ts OpenAi route +import { Configuration, OpenAIApi } from "openai-edge"; +import { OpenAIStream, StreamingTextResponse } from "ai"; +import { NextResponse } from "next/server"; + + +export const runtime = 'edge'; + +// Stores the API key of our OpenAI model +const config = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const openai = new OpenAIApi(config); + +/** + * Handles POST requests to the chat-gpt route. + * @param request - The incoming request object. + * @returns A StreamingTextResponse or NextResponse object. + */ +export async function POST(request: Request): Promise { + // Destructure the incoming request to get the messages array, model, and temperature + const { + messages, //previous messages of chat + temperature = 0.5, //temperature of chat + customInstructions = "" //custom instructions of chat + } = await request.json(); // {messages:[], model: '', temperature: 0.5} + console.log(messages); + + try { + /** + * Function that utilizes the tool "createChatCompletion". + * This defines the type of chat model that is going to be used. + * Activate or deactivate the use of streaming in the web app. + * Get the message response from OpenAI using "createChatCompletion". + */ + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + stream: true, // Enable streaming + temperature, // Set temperature, default is 0.5 if not provided + messages: [ + {role: "system", content: customInstructions}, + ...messages + ], + }); + + // Creates a stream of data from OpenAI using the tool "OpenAIStream" + const stream = OpenAIStream(response); + + // Sends the stream as a response to our user. + return new StreamingTextResponse(stream, { status: 200}); + + } catch (error: any) { + // If an error occurs, log it to the console and send a message to the user + // console.error(error); + return new NextResponse(`Error: ${error.message}`, { status: 500 }); + } +} diff --git a/apps/web/app/api/ai/openai/gpt-4/route.ts b/apps/web/app/api/ai/openai/gpt-4/route.ts index 619ac8f..f506a4a 100644 --- a/apps/web/app/api/ai/openai/gpt-4/route.ts +++ b/apps/web/app/api/ai/openai/gpt-4/route.ts @@ -2,15 +2,12 @@ import { Configuration, OpenAIApi } from "openai-edge"; import { OpenAIStream, StreamingTextResponse } from "ai"; import { NextResponse } from "next/server"; -// import { createMessage } from "@/lib/message"; -// import type {MessageDataInput} from "@/lib/message"; -// import { Sender } from "@prisma/client"; -export const runtime = 'edge'; +export const runtime = "edge"; // Stores the API key of our OpenAI model const config = new Configuration({ - apiKey: process.env.OPENAI_API_KEY, + apiKey: process.env.OPENAI_API_KEY, }); const openai = new OpenAIApi(config); @@ -20,62 +17,48 @@ const openai = new OpenAIApi(config); * @param request - The incoming request object. * @returns A StreamingTextResponse or NextResponse object. */ -export async function POST(request: Request): Promise { - // Destructure the incoming request to get the messages array, model, and temperature - const { - messages, //previous messages of chat - temperature = 0.5, //temperature of chat - customInstructions = "" //custom instructions of chat - } = await request.json(); // {messages:[], model: '', temperature: 0.5} - console.log(messages); +export async function POST( + request: Request +): Promise { + // Destructure the incoming request to get the messages array, model, and temperature + const { messages, userContext, responseContext, temperature } = + await request.json(); - try { - /** - * Function that utilizes the tool "createChatCompletion". - * This defines the type of chat model that is going to be used. - * Activate or deactivate the use of streaming in the web app. - * Get the message response from OpenAI using "createChatCompletion". - */ - const response = await openai.createChatCompletion({ - model: "gpt-4", - stream: true, // Enable streaming - temperature, // Set temperature, default is 0.5 if not provided - messages: [ - {role: "system", content: customInstructions}, - ...messages - ], - }); - - // Define the onCompletion and onToken callbacks - // const onCompletion = async (completion: any) => { - // console.log('Stream complete:', completion); - // const messageInfo: MessageDataInput = { - // idConversation: 1, - // sender: Sender.MODEL, - // content: completion, - // creditsUsed: tokens, - // }; - - // // Create a new message in the database using Prisma - // const response = createMessage(messageInfo) - // console.log(response) - // }; - - // let tokens = 0; - // const onToken = (_token: any): void => { - // tokens += 1; - // }; - - - // Creates a stream of data from OpenAI using the tool "OpenAIStream" - const stream = OpenAIStream(response); + try { + //temperature + const temp = Number(temperature); //temperature of chat + //construct Custom Instructions + let customInstructions = ""; //custom instructions of chat - // Sends the stream as a response to our user. - return new StreamingTextResponse(stream, { status: 200}); + if (responseContext !== "") { + customInstructions += `How you should respond: ${responseContext}. `; + } - } catch (error: any) { - // If an error occurs, log it to the console and send a message to the user - // console.error(error); - return new NextResponse(`Error: ${error.message}`, { status: 500 }); + if (userContext !== "") { + customInstructions += `User context: ${userContext}. `; } + + /** + * Function that utilizes the tool "createChatCompletion". + * This defines the type of chat model that is going to be used. + * Activate or deactivate the use of streaming in the web app. + * Get the message response from OpenAI using "createChatCompletion". + */ + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + stream: true, // Enable streaming + temperature: temp, // Set temperature, default is 0.5 if not provided + messages: [{ role: "system", content: customInstructions }, ...messages], + }); + + // Creates a stream of data from OpenAI using the tool "OpenAIStream" + const stream = OpenAIStream(response); + + // Sends the stream as a response to our user. + return new StreamingTextResponse(stream, { status: 200 }); + } catch (error: any) { + // If an error occurs, log it to the console and send a message to the user + // console.error(error); + return new NextResponse(`Error: ${error.message}`, { status: 500 }); + } } diff --git a/apps/web/app/api/ai/openai/route.ts b/apps/web/app/api/ai/openai/route.ts new file mode 100644 index 0000000..bdb2843 --- /dev/null +++ b/apps/web/app/api/ai/openai/route.ts @@ -0,0 +1,110 @@ +// route.ts OpenAi route +import { Configuration, OpenAIApi, type ResponseTypes } from "openai-edge"; +import { OpenAIStream, StreamingTextResponse } from "ai"; +import { NextResponse } from "next/server"; +import { uploadToLightsail } from "@/lib/helper/storage/upload-image"; + +export const runtime = "edge"; + +// Stores the API key of our OpenAI model +const config = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const openai = new OpenAIApi(config); + +/** + * Handles POST requests to the chat-gpt route. + * @param request - The incoming request object. + * @returns A StreamingTextResponse or NextResponse object. + */ +export async function POST( + request: Request +): Promise { + // Destructure the incoming request to get the messages array, model, and temperature + const { + messages, + userContext, + responseContext, + temperature, + modelName, + size, + } = await request.json(); + + if (modelName === "dalle") { + try { + /** + * Function that uses the "createImage" tool. + * Sets the prompt to the last message sent. + * Sets response format to a base64. + * Sets image size. + */ + const response: Response = await openai.createImage({ + prompt: messages[messages.length - 1].content, + response_format: "b64_json", + size, + }); + + const data = (await response.json()) as ResponseTypes["createImage"]; + + // Extracts the image string from the response data. + const img: string | undefined = data.data[0].b64_json; + + // Checks that the base64 image is not undefined and uploads the image to the lightsail bucket + // Returns the image url in markdown format + // Otherwise returns an error message + if (img) { + const imageUrl: string = await uploadToLightsail(img); + return new NextResponse(`![Generated image](${imageUrl})`, { + status: 200, + }); + } + return new NextResponse(`Error: No image was generated`, { status: 500 }); + } catch (error: any) { + // If an error occurs, log it to the console and send a message to the user + // console.error(error); + return new NextResponse(`Error: ${error.message}`, { status: 500 }); + } + } else { + try { + //temperature + const temp = Number(temperature); //temperature of chat + //construct Custom Instructions + let customInstructions = ""; //custom instructions of chat + + if (responseContext !== "") { + customInstructions += `How you should respond: ${responseContext}. `; + } + + if (userContext !== "") { + customInstructions += `User context: ${userContext}. `; + } + + /** + * Function that utilizes the tool "createChatCompletion". + * This defines the type of chat model that is going to be used. + * Activate or deactivate the use of streaming in the web app. + * Get the message response from OpenAI using "createChatCompletion". + */ + const response = await openai.createChatCompletion({ + model: modelName, + stream: true, // Enable streaming + temperature: temp, // Set temperature, default is 0.5 if not provided + messages: [ + { role: "system", content: customInstructions }, + ...messages, + ], + }); + + // Creates a stream of data from OpenAI using the tool "OpenAIStream" + const stream = OpenAIStream(response); + + // Sends the stream as a response to our user. + return new StreamingTextResponse(stream, { status: 200 }); + } catch (error: any) { + // If an error occurs, log it to the console and send a message to the user + // console.error(error); + return new NextResponse(`Error: ${error.message}`, { status: 500 }); + } + } +} diff --git a/apps/web/app/api/auth/[auth0]/route.ts b/apps/web/app/api/auth/[auth0]/route.ts index 5e63930..172de1b 100644 --- a/apps/web/app/api/auth/[auth0]/route.ts +++ b/apps/web/app/api/auth/[auth0]/route.ts @@ -1,17 +1,19 @@ -import { handleAuth, handleLogin } from "@auth0/nextjs-auth0"; +import { handleAuth } from "@auth0/nextjs-auth0"; -export const GET = handleAuth({ - login: handleLogin({ - authorizationParams: { - prompt: "login", - }, - returnTo: "/conversation/new", - }), - signup: handleLogin({ - authorizationParams: { - prompt: "login", - screen_hint: "signup", - }, - returnTo: "/", // This is the default behavior - }), -}); +// export const GET = handleAuth({ +// login: handleLogin({ +// authorizationParams: { +// prompt: "login", +// }, +// returnTo: "/conversation/new", +// }), +// signup: handleLogin({ +// authorizationParams: { +// prompt: "signup", +// screen_hint: "signup", +// }, +// returnTo: "/", // This is the default behavior +// }), +// }); + +export const GET = handleAuth(); diff --git a/apps/web/app/api/conversations/[id]/route.ts b/apps/web/app/api/conversations/[id]/route.ts index 1aeaae7..794a4a6 100644 --- a/apps/web/app/api/conversations/[id]/route.ts +++ b/apps/web/app/api/conversations/[id]/route.ts @@ -1,7 +1,10 @@ import { NextResponse } from "next/server"; -import { getConversationById, updateConversationById, deleteConversationById } from "@/lib/conversation"; -import type { ConversationUpdateData } from "@/types/conversation-types"; - +import type { UpdatedInfo } from "@/lib/conversation"; +import { + getConversationById, + updateConversationById, + deleteConversationById, +} from "@/lib/conversation"; /** * GET request handler for conversation by ID. * @param _request - The request object. @@ -9,25 +12,28 @@ import type { ConversationUpdateData } from "@/types/conversation-types"; * @returns A Promise that resolves to a NextResponse object with the conversation data or an error message. */ export async function GET( - _request: Request, // The incoming HTTP request object - { params }: { params: { id: number } } // Destructure 'params' to get 'id' from the URL -): Promise { // The function returns a Promise that resolves to a NextResponse object - - // Extract 'id' from 'params' and convert it to a Number - const id = params.id; - const idConversation = Number(id); - - // Call the 'getConversationById' function to fetch the conversation by its ID - const result = await getConversationById(idConversation); - - // Check the status code of the result - if (result.status !== 200) { - // If the status code is not 200 OK, return an error response - return new NextResponse(result.message || "An error occurred", { status: result.status }); - } - - // If everything is fine, return the conversation data as JSON - return NextResponse.json(result.data, { status: 200 }); + _request: Request, // The incoming HTTP request object + { params }: { params: { id: number } } // Destructure 'params' to get 'id' from the URL +): Promise { + // The function returns a Promise that resolves to a NextResponse object + + // Extract 'id' from 'params' and convert it to a Number + const id = params.id; + const idConversation = Number(id); + + // Call the 'getConversationById' function to fetch the conversation by its ID + const result = await getConversationById(idConversation); + + // Check the status code of the result + if (result.status !== 200) { + // If the status code is not 200 OK, return an error response + return new NextResponse(result.message || "An error occurred", { + status: result.status, + }); + } + + // If everything is fine, return the conversation data as JSON + return NextResponse.json(result.data, { status: 200 }); } /** @@ -37,29 +43,32 @@ export async function GET( * @returns A Promise that resolves to a NextResponse object with the updated conversation data. */ export async function PATCH( - request: Request, // The incoming HTTP request object - { params }: { params: { id: number } } // Destructure 'params' to get 'id' from the URL -): Promise { // The function returns a Promise that resolves to a NextResponse object - - // Extract 'id' from 'params' and convert it to a Number - const id = params.id; - const idConversation = Number(id); - - // Parse the request body to get the updated information for the conversation - const body = await request.text(); - const updatedInfo: ConversationUpdateData = body ? JSON.parse(body) : {}; - - // Call the 'updateConversationById' function to update the conversation in the database - const result = await updateConversationById(idConversation, updatedInfo); - - // Check the status code of the result - if (result.status !== 200) { - // If the status code is not 200 OK, return an error response - return new NextResponse(result.message || "An error occurred", { status: result.status }); - } - - // If everything is fine, return the updated conversation data as JSON - return NextResponse.json(result.data, { status: 200 }); + request: Request, // The incoming HTTP request object + { params }: { params: { id: number } } // Destructure 'params' to get 'id' from the URL +): Promise { + // The function returns a Promise that resolves to a NextResponse object + + // Extract 'id' from 'params' and convert it to a Number + const id = params.id; + const idConversation = Number(id); + + // Parse the request body to get the updated information for the conversation + const body: UpdatedInfo = await request.json(); + //const updatedInfo: UpdatedInfo = body ? JSON.parse(body) : {}; + + // Call the 'updateConversationById' function to update the conversation in the database + const result = await updateConversationById(idConversation, body); + + // Check the status code of the result + if (result.status !== 200) { + // If the status code is not 200 OK, return an error response + return new NextResponse(result.message || "An error occurred", { + status: result.status, + }); + } + + // If everything is fine, return the updated conversation data as JSON + return NextResponse.json(result.data, { status: 200 }); } /** @@ -69,23 +78,29 @@ export async function PATCH( * @returns A promise that resolves to a NextResponse object with a success message or an error message and status code. */ export async function DELETE( - _request: Request, // The incoming HTTP request object (not used in this function) - { params }: { params: { id: number } } // Destructure 'params' to get 'id' from the URL -): Promise { // The function returns a Promise that resolves to a NextResponse object - - // Extract 'id' from 'params' and convert it to a Number - const id = params.id; - const idConversation = Number(id); - - // Call the 'deleteConversationById' function to delete the conversation and its messages from the database - const result = await deleteConversationById(idConversation); - - // Check the status code of the result - if (result.status !== 200) { - // If the status code is not 200 OK, return an error response - return new NextResponse(result.message || "An error occurred", { status: result.status }); - } - - // If everything is fine, return a success message as JSON - return NextResponse.json({ message: "Conversation and messages deleted successfully" }, { status: 200 }); -} \ No newline at end of file + _request: Request, // The incoming HTTP request object (not used in this function) + { params }: { params: { id: number } } // Destructure 'params' to get 'id' from the URL +): Promise { + // The function returns a Promise that resolves to a NextResponse object + + // Extract 'id' from 'params' and convert it to a Number + const id = params.id; + const idConversation = Number(id); + + // Call the 'deleteConversationById' function to delete the conversation and its messages from the database + const result = await deleteConversationById(idConversation); + + // Check the status code of the result + if (result.status !== 200) { + // If the status code is not 200 OK, return an error response + return new NextResponse(result.message || "An error occurred", { + status: result.status, + }); + } + + // If everything is fine, return a success message as JSON + return NextResponse.json( + { message: "Conversation and messages deleted successfully" }, + { status: 200 } + ); +} diff --git a/apps/web/app/api/conversations/route.ts b/apps/web/app/api/conversations/route.ts index 80f6f9a..4f8c2fb 100644 --- a/apps/web/app/api/conversations/route.ts +++ b/apps/web/app/api/conversations/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import {createConversation} from "@/lib/conversation"; import type { ConversationCreateData } from "@/types/conversation-types"; +import type { PrismaResponse } from "@/types/prisma-client-types"; +import type { SidebarConversation } from "@/types/sidebar-conversation-types"; /** * Handles POST requests to create a new conversation. @@ -15,7 +17,7 @@ request: Request // The incoming HTTP request object const input: ConversationCreateData = JSON.parse(await request.text()); // Call the 'createConversation' function to create a new conversation in the database - const result = await createConversation(input); + const result: PrismaResponse = await createConversation(input); // Check the status code of the result if (result.status !== 201) { diff --git a/apps/web/app/api/groups/[id]/route.ts b/apps/web/app/api/groups/[id]/route.ts index 9438fad..98415df 100644 --- a/apps/web/app/api/groups/[id]/route.ts +++ b/apps/web/app/api/groups/[id]/route.ts @@ -1,75 +1,95 @@ -import { Group } from "@prisma/client"; +import type { Group } from "@prisma/client"; import { NextResponse } from "next/server"; import type { PrismaResponse } from "@/types/prisma-client-types"; import { deleteGroup, getGroupById, updateGroupById } from "@/lib/group"; import { isGroupUpdateData } from "@/types/group-types"; /** - * GET request handler, fetches a specific group. + * GET request handler, fetches a specific group. * @param request - The request object. - * @param params - The request's parameters, which contain the ID of the group to fetch. - * @returns A Promise that resolves to a NextResponse instance, that potentially has as body the fetched group. -*/ -export async function GET(request: Request, {params: {id}}: {params:{id: number}}): Promise { - const prismaResponse: PrismaResponse = await getGroupById(Number(id)) + * @param params - The request's parameters, which contain the ID of the group to fetch. + * @returns A Promise that resolves to a NextResponse instance, that potentially has as body the fetched group. + */ +export async function GET( + request: Request, + { params: { id } }: { params: { id: number } } +): Promise { + const prismaResponse: PrismaResponse = await getGroupById(Number(id)); - if (prismaResponse.status === 200 && prismaResponse.data){ - return NextResponse.json(prismaResponse.data, {status: 200, statusText: 'OK'}) - } + if (prismaResponse.status === 200 && prismaResponse.data) { + return NextResponse.json(prismaResponse.data, { + status: 200, + statusText: "OK", + }); + } - return new NextResponse(prismaResponse.message || "Unknown Error", { - status: prismaResponse.status, - headers: {'Content-Type': 'text/plain'} - }) + return new NextResponse(prismaResponse.message || "Unknown Error", { + status: prismaResponse.status, + headers: { "Content-Type": "text/plain" }, + }); } /** - * DELETE request handler, deletes a specific group. + * DELETE request handler, deletes a specific group. * @param request - The request object. - * @param params - The request's parameters, which contain the ID of the group to delete. - * @returns A Promise that resolves to a NextResponse instance, potentially holding in its body the deleted group. -*/ + * @param params - The request's parameters, which contain the ID of the group to delete. + * @returns A Promise that resolves to a NextResponse instance, potentially holding in its body the deleted group. + */ -export async function DELETE(request: Request, {params: {id}}: {params:{id: number}}): Promise { - const prismaResponse: PrismaResponse = await deleteGroup(Number(id)) +export async function DELETE( + request: Request, + { params: { id } }: { params: { id: number } } +): Promise { + const prismaResponse: PrismaResponse = await deleteGroup(Number(id)); - if (prismaResponse.status === 200 && prismaResponse.data){ - return NextResponse.json(prismaResponse.data, {status: 200, statusText: 'OK'}) - } + if (prismaResponse.status === 200 && prismaResponse.data) { + return NextResponse.json(prismaResponse.data, { + status: 200, + statusText: "OK", + }); + } - return new NextResponse(prismaResponse.message || "Unknown Error", { - status: prismaResponse.status, - headers: {'Content-Type': 'text/plain'} - }) + return new NextResponse(prismaResponse.message || "Unknown Error", { + status: prismaResponse.status, + headers: { "Content-Type": "text/plain" }, + }); } /** - * PATCH request handler, updated a specific group. + * PATCH request handler, updated a specific group. * @param request - The request object. - * @param params - The request's parameters, which contain the ID of the group to update. - * @returns A Promise that resolves to a NextResponse instance, potentially holding in its body the updated group. -*/ + * @param params - The request's parameters, which contain the ID of the group to update. + * @returns A Promise that resolves to a NextResponse instance, potentially holding in its body the updated group. + */ -export async function PATCH(request: Request, {params: {id}}: {params:{id: number}}): Promise { - const requestBody = await request.json() +export async function PATCH( + request: Request, + { params: { id } }: { params: { id: number } } +): Promise { + const requestBody = await request.json(); - if (isGroupUpdateData(requestBody)){//verify that the given data object implements the updateGroupById interface - const prismaResponse: PrismaResponse = await updateGroupById(Number(id), requestBody) - if (prismaResponse.status === 200 && prismaResponse.data){ - return NextResponse.json(prismaResponse.data, {status: 200, statusText: 'OK'}) - } - - return new NextResponse(prismaResponse.message || "Unknown Error", { - status: prismaResponse.status, - headers: {'Content-Type': 'text/plain'} - }) + if (isGroupUpdateData(requestBody)) { + //verify that the given data object implements the updateGroupById interface + const prismaResponse: PrismaResponse = await updateGroupById( + Number(id), + requestBody + ); + if (prismaResponse.status === 200 && prismaResponse.data) { + return NextResponse.json(prismaResponse.data, { + status: 200, + statusText: "OK", + }); } - return new NextResponse("Invalid input in the request's body", { - status: 400, - statusText: "Bad Request", - headers: { 'Content-Type': 'text/plain'} - }) - -} + return new NextResponse(prismaResponse.message || "Unknown Error", { + status: prismaResponse.status, + headers: { "Content-Type": "text/plain" }, + }); + } + return new NextResponse("Invalid input in the request's body", { + status: 400, + statusText: "Bad Request", + headers: { "Content-Type": "text/plain" }, + }); +} diff --git a/apps/web/app/api/groups/add-users/[id]/route.ts b/apps/web/app/api/groups/add-users/[id]/route.ts new file mode 100644 index 0000000..c5f0b02 --- /dev/null +++ b/apps/web/app/api/groups/add-users/[id]/route.ts @@ -0,0 +1,97 @@ + +import { NextResponse } from 'next/server'; +import type { Group, User } from '@prisma/client'; +import { addUsersToGroup, removeUsersFromGroup } from '@/lib/group'; +import type { PrismaResponse } from '@/types/prisma-client-types'; + +export async function POST(request: Request, { params: { id } }: { params: { id: string } }): Promise { + try { + // Parse the request body + const body = await request.json(); + + // Validate that the body contains an array of user IDs + if (!Array.isArray(body.userIds)) { + return new NextResponse('Invalid input: userIds must be an array', { + status: 400, + headers: { 'Content-Type': 'text/plain' }, + }); + } + + const userIds: number[] = body.userIds; + + // Call the addUsersToGroup function + const prismaResponse: PrismaResponse<{ group: Group; users: User[] }> = await addUsersToGroup(Number(id), userIds); + + if (prismaResponse.status === 200 && prismaResponse.data) { + return NextResponse.json(prismaResponse.data, { + status: 200, + statusText: 'OK', + }); + } + + return new NextResponse(prismaResponse.message || 'Unknown Error', { + status: prismaResponse.status, + headers: { 'Content-Type': 'text/plain' }, + }); + } catch (error: any) { + console.error('Error processing request:', error.message); + return new NextResponse('Server Error', { + status: 500, + headers: { 'Content-Type': 'text/plain' }, + }); + } +} + + +/** + * PATCH request handler, updates a specific group by removing users. + * @param request - The request object. + * @param params - The request's parameters, which contain the ID of the group. + * @returns A Promise that resolves to a NextResponse instance, potentially holding the result of the operation. + */ +export async function PATCH( + request: Request, + { params: { id } }: { params: { id: string } } + ): Promise { + + const idGroup = Number(id); + + + if (isNaN(idGroup)) { + return new NextResponse("Invalid group ID", { + status: 400, + headers: { "Content-Type": "text/plain" }, + }); + } + + + // Extract user IDs from request body + const body = await request.json(); + const userIds: string[] = body.userIds; + const userIdsAsNumbers: number[] = userIds.map(idUser => Number(idUser)); + + + + if (!userIdsAsNumbers || !Array.isArray(userIdsAsNumbers)) { + return new NextResponse("Invalid user IDs", { + status: 400, + headers: { "Content-Type": "text/plain" }, + }); + } + + + const prismaResponse: PrismaResponse<{ group: Group; users: User[] }> = + await removeUsersFromGroup(idGroup, userIdsAsNumbers); + + if (prismaResponse.status === 200 && prismaResponse.data) { + return NextResponse.json(prismaResponse.data, { + status: 200, + statusText: "OK", + }); + } + + return new NextResponse(prismaResponse.message || "Unknown Error", { + status: prismaResponse.status, + headers: { "Content-Type": "text/plain" }, + }); + } \ No newline at end of file diff --git a/apps/web/app/api/groups/modifyCredits/[id]/route.ts b/apps/web/app/api/groups/modifyCredits/[id]/route.ts new file mode 100644 index 0000000..0de701c --- /dev/null +++ b/apps/web/app/api/groups/modifyCredits/[id]/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server" +import { modifyGroupsCurrentCredits } from "@/lib/group" +import type { PrismaResponse } from "@/types/prisma-client-types" + +export async function PATCH(request: Request, {params: {id}}: {params: {id: number}}): Promise { + const requestBody = await request.json() + + if (requestBody?.creditOffset){ + const prismaResponse: PrismaResponse<{count: number}> = await modifyGroupsCurrentCredits(Number(id), Number(requestBody?.creditOffset)) + + if (prismaResponse.status === 200 && prismaResponse.data){ + return NextResponse.json(prismaResponse.data, {status: 200, statusText: 'OK'}) + } + + return new NextResponse(prismaResponse.message || "Unknown Error", { + status: prismaResponse.status, + headers: { 'Content-Type': 'text/plain' } + }) + } + + return new NextResponse("Invalid input value in the request's body", { + status: 400, + statusText: "Bad Request", + headers: { 'Content-Type': 'text/plain' } + }) +} \ No newline at end of file diff --git a/apps/web/app/api/keys/[id]/route.ts b/apps/web/app/api/keys/[id]/route.ts index e69de29..cb0ff5c 100644 --- a/apps/web/app/api/keys/[id]/route.ts +++ b/apps/web/app/api/keys/[id]/route.ts @@ -0,0 +1 @@ +export {}; diff --git a/apps/web/app/api/keys/route.ts b/apps/web/app/api/keys/route.ts index e69de29..cb0ff5c 100644 --- a/apps/web/app/api/keys/route.ts +++ b/apps/web/app/api/keys/route.ts @@ -0,0 +1 @@ +export {}; diff --git a/apps/web/app/api/keys/user/[id]/route.ts b/apps/web/app/api/keys/user/[id]/route.ts index e69de29..cb0ff5c 100644 --- a/apps/web/app/api/keys/user/[id]/route.ts +++ b/apps/web/app/api/keys/user/[id]/route.ts @@ -0,0 +1 @@ +export {}; diff --git a/apps/web/app/api/params/[id]/route.ts b/apps/web/app/api/params/[id]/route.ts index e69de29..cb0ff5c 100644 --- a/apps/web/app/api/params/[id]/route.ts +++ b/apps/web/app/api/params/[id]/route.ts @@ -0,0 +1 @@ +export {}; diff --git a/apps/web/app/api/params/conversation/[id]/route.ts b/apps/web/app/api/params/conversation/[id]/route.ts index e69de29..cb0ff5c 100644 --- a/apps/web/app/api/params/conversation/[id]/route.ts +++ b/apps/web/app/api/params/conversation/[id]/route.ts @@ -0,0 +1 @@ +export {}; diff --git a/apps/web/app/api/params/route.ts b/apps/web/app/api/params/route.ts index e69de29..cb0ff5c 100644 --- a/apps/web/app/api/params/route.ts +++ b/apps/web/app/api/params/route.ts @@ -0,0 +1 @@ +export {}; diff --git a/apps/web/app/api/users/[id]/route.ts b/apps/web/app/api/users/[id]/route.ts index 4c56a8a..78baacb 100644 --- a/apps/web/app/api/users/[id]/route.ts +++ b/apps/web/app/api/users/[id]/route.ts @@ -2,7 +2,7 @@ import type { User } from "@prisma/client"; import { NextResponse } from "next/server"; import type { PrismaResponse } from "@/types/prisma-client-types"; import { deleteUser, getUser, updateUser } from "@/lib/user"; -import { isUserUpdateData } from "@/types/user-types"; +import { isUserUpdateData, type UserUpdateData } from "@/types/user-types"; /** @@ -50,10 +50,13 @@ export async function DELETE(request: Request, {params: {id}}: {params: {id: num * @returns A Promise that resolves to a NextResponse instance, potentially holding in its body the updated user. */ export async function PATCH(request: Request, {params: {id}}: {params: {id: number}}): Promise { - const requestBody = await request.json() + const requestBody:UserUpdateData = await request.json() + console.log("requestBody: ", requestBody); + console.log("===================================="); - if (isUserUpdateData(requestBody)){ // Verify that the given input data object implements the UserUpdateData interface. - const prismaResponse: PrismaResponse = await updateUser(Number(id), requestBody) + if (isUserUpdateData(requestBody)){ // Verify that the given input data object implements the UserUpdateData interface. + + const prismaResponse: PrismaResponse = await updateUser(Number(id), requestBody); if (prismaResponse.status === 200 && prismaResponse.data){ return NextResponse.json(prismaResponse.data, {status: 200, statusText: 'OK'}) diff --git a/apps/web/app/api/users/group/[id]/route.ts b/apps/web/app/api/users/group/[id]/route.ts new file mode 100644 index 0000000..4d05ea5 --- /dev/null +++ b/apps/web/app/api/users/group/[id]/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from "next/server"; +import type { User } from "@prisma/client"; +import type { PrismaResponse } from "@/types/prisma-client-types"; +import { getUsersNotInGroup } from "@/lib/user"; // Adjust the import path as necessary + +/** + * GET request handler, fetches all users not in a specific group. + * @param request - The request object. + * @param params - The request's parameters, which contain the ID of the group. + * @returns A Promise that resolves to a NextResponse instance, potentially holding the users not in the group. + */ +export async function GET( + request: Request, {params: {id}}: {params: {id: string}} +): Promise { + const groupID = Number(id); + + if (isNaN(groupID)) { + return new NextResponse("Invalid group ID", { + status: 400, + headers: { "Content-Type": "text/plain" }, + }); + } + + const prismaResponse: PrismaResponse = + await getUsersNotInGroup(groupID); + + if (prismaResponse.status === 200 && prismaResponse.data) { + return NextResponse.json(prismaResponse.data, { + status: 200, + statusText: "OK", + }); + } + + return new NextResponse(prismaResponse.message || "Unknown Error", { + status: prismaResponse.status, + headers: { "Content-Type": "text/plain" }, + }); +} diff --git a/apps/web/app/api/users/removeCredits/[id]/route.ts b/apps/web/app/api/users/removeCredits/[id]/route.ts new file mode 100644 index 0000000..1d7da71 --- /dev/null +++ b/apps/web/app/api/users/removeCredits/[id]/route.ts @@ -0,0 +1,24 @@ +import type { User } from "@prisma/client"; +import { NextResponse } from "next/server"; +import { decrementUserCreditsRemaining } from "@/lib/user"; +import type { PrismaResponse } from "@/types/prisma-client-types"; + +export async function PATCH(request: Request, {params: {id}}: {params: {id: number}}): Promise { + const reqBody = await request.json() + + if (reqBody?.creditReduction === undefined){ + return new NextResponse("Invalid request body", {status: 400, headers: {"Content-Type": "text/plain"}}) + } + + const prismaResponse: PrismaResponse = await decrementUserCreditsRemaining(Number(id), Number(reqBody?.creditReduction)) + + if (prismaResponse.status !== 200 || !prismaResponse.data){ + const responseOptions: ResponseInit = { + status: prismaResponse.status, + headers: {"Content-Type": "text/plain"} + } + return new NextResponse(prismaResponse.message ?? "Operation failed", responseOptions) + } + + return NextResponse.json(prismaResponse.data, {status: 200}) +} \ No newline at end of file diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index f528183..f0e1e20 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -5,7 +5,7 @@ @layer components { /* main */ .main-container { - @apply flex min-h-screen flex-1 flex-col items-center px-6 pb-10 pt-28 max-md:pb-32 sm:px-10; + @apply flex min-h-screen flex-1 flex-col items-center p-0 max-md:pb-32; } /* Head Text */ @@ -64,6 +64,8 @@ .rightsidebar { @apply sticky right-0 top-0 z-20 flex h-screen w-fit flex-col justify-between gap-12 overflow-auto border-l border-l-dark-4 px-10 pb-6 pt-28 max-xl:hidden; } + + } @layer utilities { @@ -89,6 +91,35 @@ .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #7878a3; } + + .gradient-shadow-light { + background: linear-gradient(to top, rgb(255, 255, 255) 70%, rgba(255, 255, 255, 0) 100%); + } + .gradient-shadow-light-conversation-card { + background: linear-gradient(to left, rgb(255, 255, 255, 0.9) 70%, rgba(255, 255, 255, 0) 100%); + } + + /* Message Backgrounds*/ + .senderUser-bg { + @apply bg-black bg-opacity-0 dark:bg-white dark:bg-opacity-10; + } + + .senderModel-bg { + @apply bg-black bg-opacity-5 dark:bg-white dark:bg-opacity-5; + } +} + +/* Dark mode */ +@media (prefers-color-scheme: dark) { + @layer utilities { + .gradient-shadow-dark { + background: linear-gradient(to top, rgb(0, 0, 0) 70%, rgba(0, 0, 0, 0) 100%); + } + + .gradient-shadow-dark-conversation-card { + background: linear-gradient(to left, rgba(34, 34, 34, 0.9) 75%, rgba(0, 0, 0, 0) 100%); + } + } } /* Shadcn Component Styles */ diff --git a/apps/web/components/admin/adminSection1/organisms/placeholder.tsx b/apps/web/components/admin/adminSection1/organisms/placeholder.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/apps/web/components/admin/adminSection2/atoms/placeholder.tsx b/apps/web/components/admin/adminSection2/atoms/placeholder.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/apps/web/components/admin/adminSection2/molecules/placeholder.tsx b/apps/web/components/admin/adminSection2/molecules/placeholder.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/apps/web/components/admin/adminSection2/organisms/placeholder.tsx b/apps/web/components/admin/adminSection2/organisms/placeholder.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/apps/web/components/admin/editGroup/atoms/numeric-stepper.tsx b/apps/web/components/admin/editGroup/atoms/numeric-stepper.tsx new file mode 100644 index 0000000..ca8b9c1 --- /dev/null +++ b/apps/web/components/admin/editGroup/atoms/numeric-stepper.tsx @@ -0,0 +1,19 @@ +import { LuPlusCircle, LuMinusCircle } from "react-icons/lu" + +interface NumericStepperProps { + onArrowUpPress: () => void; + onArrowDownPress: () => void; +} + +export default function NumericStepper({onArrowUpPress, onArrowDownPress}: NumericStepperProps): JSX.Element { + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/apps/web/components/admin/editGroup/molcules/credit-input.tsx b/apps/web/components/admin/editGroup/molcules/credit-input.tsx new file mode 100644 index 0000000..cdcd384 --- /dev/null +++ b/apps/web/components/admin/editGroup/molcules/credit-input.tsx @@ -0,0 +1,41 @@ +import { BiCoinStack } from "react-icons/bi"; +import { Input } from "@nextui-org/react"; +import { strToNumber } from "@/helpers/string-helpers"; +import NumericStepper from "../atoms/numeric-stepper"; + +interface CreditInputProps { + credits: string; + onCreditsChange: (newCredits: string) => void; +} + +export default function CreditInput({credits, onCreditsChange}: CreditInputProps): JSX.Element { + const stepValue = 1 + + const handleCreditsValueChange: (value: string) => void = (value) => { + onCreditsChange(value) + } + + const handleArrowUpPress: () => void = () => { + credits.length === 0 ? onCreditsChange(stepValue.toString()) : onCreditsChange((strToNumber(credits) + stepValue).toString()) + } + + const handleArrowDownPress: () => void = () => { + credits.length === 0 ? onCreditsChange((-stepValue).toString()) : onCreditsChange((strToNumber(credits) - stepValue).toString()) + } + + return ( + + } + onValueChange={handleCreditsValueChange} + placeholder="Group credits" + startContent={} + value={credits} + /> + ); +} \ No newline at end of file diff --git a/apps/web/components/admin/editGroup/molcules/edit-group-menu-modal.tsx b/apps/web/components/admin/editGroup/molcules/edit-group-menu-modal.tsx new file mode 100644 index 0000000..49b603d --- /dev/null +++ b/apps/web/components/admin/editGroup/molcules/edit-group-menu-modal.tsx @@ -0,0 +1,187 @@ +import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/react"; +import { useEffect, useState } from "react"; +import type { Group } from "@prisma/client"; +import { toast } from "sonner"; +import { AiFillDelete } from "react-icons/ai" +import { groupsAreEqual, isValidGroup } from "@/helpers/group-helpers"; +import ConfirmDeleteModal from "@/components/shared/molecules/confirm-delete-modal"; +import NewGroupMenu from "./edit-group-menu"; + +interface EditGroupMenuModalProps { + isNew: boolean; + initialGroup: Group; + isOpen: boolean; + onGroupSave: (savedGroup: Group) => void; + onGroupDeletion?: (deletedGroup: Group) => void; + onModalClose: () => void; +} + +export default function EditGroupMenuModal({isNew, initialGroup, isOpen, onGroupSave, onGroupDeletion, onModalClose}: EditGroupMenuModalProps): JSX.Element { + const [group, setGroup] = useState(initialGroup) + const [isLoading, setIsLoading] = useState(false) + const [confirmDeleteModalIsOpen, setConfirmDeleteModalIsOpen] = useState(false) + const modalHorizontalPadding = 5 + const saveIsDisabled = !isValidGroup(group) || groupsAreEqual(initialGroup, group) + + useEffect(() => { + if (!isOpen){ + setGroup(initialGroup) + } + }, [initialGroup, isOpen]) + + const handleGroupChange: (editedGroup: Group) => void = (editedGroup) => { + setGroup(editedGroup) + } + + const handleSaveButtonPress: (e: any) => void = (_) => { + if (!saveIsDisabled && !isLoading){ + setIsLoading(true) + isNew ? createGroup() : editGroup() + } + } + + const handleDeleteButtonPress: (e: any) => void = (_) => { + if (onGroupDeletion && !isNew){ + onModalClose() + setConfirmDeleteModalIsOpen(true) + } + } + + const handleConfirmDeleteModalClosing: (confirm: boolean) => void = (confirm) => { + if (confirm){ + deleteGroup() + } + + setConfirmDeleteModalIsOpen(false) + } + + const createGroup: () => void = () => { + const fetchOptions: RequestInit = { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({...group, id: undefined}) + } + + fetch("/api/groups", fetchOptions) + .then((response) => { + if (!response.ok){ + throw new Error("Network response was not ok") + } + return response.json() + }) + .then((createdGroup) => { + onGroupSave(createdGroup as Group) + + toast.success("Group created.") + onModalClose() + }) + .catch((_) => { + toast.error("Failed to create the group.") + }) + .finally(() => {setIsLoading(false)}) + } + + const editGroup: () => void = () => { + const fetchOptions: RequestInit = { + method: "PATCH", + headers: {"Content-type": "application/json", }, + body: JSON.stringify(group) + } + + fetch(`/api/groups/${group.id}`, fetchOptions) + .then((response) => { + if (!response.ok){ + throw new Error("Network response was not ok") + } + return response.json() + }) + .then((editedGroup) => { + onGroupSave(editedGroup as Group) + + toast.success("Group edited.") + onModalClose() + }) + .catch((_) => { + toast.error(`Failed to edit the group.`) + }) + .finally(() => {setIsLoading(false)}) + } + + const deleteGroup: () => void = () => { + const fetchOptions: RequestInit = {method: "DELETE"} + + fetch(`/api/groups/${group.id}`, fetchOptions) + .then((response) => { + if (!response.ok){ + throw new Error("Network response was not ok") + } + return response.json() + }) + .then((deletedGroup) => { + if (onGroupDeletion){ + onGroupDeletion(deletedGroup as Group) + toast.success("Group deleted successfully.") + } + }) + .catch((_) => { + toast.error(`Failed to delete the group.`) + }) + } + + const handleModalClose: () => void = () => { + onModalClose() + } + + return ( + <> + + + {(onClose) => ( + <> + +
+

{isNew ? "New group" : "Edit group"}

+ + {!isNew ? + + : null} +
+
+ + + + +
+ + + +
+
+ + )} +
+
+ + + + ); +} \ No newline at end of file diff --git a/apps/web/components/admin/editGroup/molcules/edit-group-menu.tsx b/apps/web/components/admin/editGroup/molcules/edit-group-menu.tsx new file mode 100644 index 0000000..13dcce6 --- /dev/null +++ b/apps/web/components/admin/editGroup/molcules/edit-group-menu.tsx @@ -0,0 +1,63 @@ +import { Input, Textarea } from "@nextui-org/react"; +import type { Group } from "@prisma/client"; +import { useEffect, useState } from "react"; +import { editGroupCredits, editGroupDescription, editGroupName } from "@/helpers/group-helpers"; +import { imposeMaxLength, strToNumber, trimLeadingSpaces, isPositiveDecimal } from "@/helpers/string-helpers"; +import TextInfoTooltip from "../../shared/molecules/text-info-tooltip"; +import CreditInput from "./credit-input"; + +interface EditGroupMenuProps { + isEditing: boolean; + group: Group; + onGroupChange: (editedGroup: Group) => void; +} + +export default function EditGroupMenu({isEditing, group, onGroupChange}: EditGroupMenuProps): JSX.Element { + const [creditsString, setCreditsString] = useState(group.creditsAssigned.toString()) + const groupNameMaxLength = 20 + + useEffect(() => { + if (!isEditing){ + setCreditsString(group.creditsAssigned.toString()) + } + }, [group.creditsAssigned, isEditing]) + + const handleGroupNameChange: (value: string) => void = (value) => { + onGroupChange(editGroupName(group, imposeMaxLength(trimLeadingSpaces(value), groupNameMaxLength))) + } + + const handleGroupDescriptionChange: (value: string) => void = (value) => { + onGroupChange(editGroupDescription(group, trimLeadingSpaces(value))) + } + + const handleGroupCreditsChange: (value: string) => void = (value) => { + const trimmedValue: string = trimLeadingSpaces(value) + if (trimmedValue.length === 0 || isPositiveDecimal(trimmedValue)){ + setCreditsString(trimmedValue) + onGroupChange(editGroupCredits(group, strToNumber(trimmedValue))) + } + } + + return ( +
+
+

Name

+ +
+ +
+
+

Monthly credits

+ +
+ + +
+ +
+

Description

+