diff --git a/app/designSystem/landing/ChatBarForm.tsx b/app/designSystem/landing/ChatBarForm.tsx new file mode 100644 index 0000000..15215d4 --- /dev/null +++ b/app/designSystem/landing/ChatBarForm.tsx @@ -0,0 +1,64 @@ +import { InputHTMLAttributes, useState } from 'react' +import { Api } from '~/core/trpc' + +interface ChatBarFormProps extends InputHTMLAttributes { + onNewMessage: (message: any) => void +} + +/** + * Form component for the chat input bar, allowing users to type and submit messages. + */ +export const ChatBarForm = ({ + className, + onNewMessage, + ...remainingProps +}: ChatBarFormProps) => { + const { mutateAsync:login } = Api.authentication.login.useMutation(); + const { mutateAsync:generateText } = Api.ai.generateText.useMutation(); + + const handleLogin = async () => { + await login({ + email: 'test@test.com', + password: 'password', + }); + }; + + const handleGenerateText = async (promptMessage: string) => { + return generateText({prompt: promptMessage}) + }; + + const [message, setMessage] = useState('') + + const handleSubmit = async (event: React.FormEvent) => { + onNewMessage({ ai: false, text: message }); + event.preventDefault() + setMessage(''); + await handleLogin(); + // Handle form submission logic here + await handleGenerateText(message).then((response) => { + if(response.answer.includes('') && response.answer.includes('')) { + const pdfFile = response.answer.split('')[1].split('')[0]; + onNewMessage({ ai: true, text: 'Here is the pdf!', pdfFile }); + } else { + onNewMessage({ ai: true, text: response.answer }); + } + }); + } + + const handleChange = (event: React.ChangeEvent) => { + setMessage(event.target.value); + }; + + return ( +
+ + +
+ ) +} diff --git a/app/designSystem/landing/ChatHistory.tsx b/app/designSystem/landing/ChatHistory.tsx new file mode 100644 index 0000000..3c1641d --- /dev/null +++ b/app/designSystem/landing/ChatHistory.tsx @@ -0,0 +1,77 @@ +import { HTMLAttributes } from 'react' +import { DesignSystemUtility } from '../helpers/utility' +import LandingButton from './LandingButton' +import { LandingAvatar } from './LandingAvatar' +import { jsPDF } from 'jspdf' + +interface Props extends HTMLAttributes { + messages: MessageHistory[] + title?: string + subtitle?: string + buttonText?: string + buttonLink?: string +} + +interface MessageHistory { + ai: boolean + text: string, + pdfFile?: string +} + +export const ChatHistory: React.FC = ({ + messages, + title, + subtitle, + buttonText, + buttonLink, + className, + ...props +}) => { + + const handleDownload = (index) => { + const pdfFile = messages[index].pdfFile; + if (pdfFile) { + const doc = new jsPDF(); + doc.text(pdfFile, 10, 10); + const pdfBlob = doc.output('blob'); + const url = window.URL.createObjectURL(pdfBlob); + const link = document.createElement('a'); + link.href = url; + link.target = '_blank'; + link.click(); + window.URL.revokeObjectURL(url); + } + } + + return ( +
+
+

{title}

+
+ {messages.map((message,index) => ( + + ))} +
+
+
+ ) +} diff --git a/app/designSystem/landing/LandingAvatar.tsx b/app/designSystem/landing/LandingAvatar.tsx index 8040b13..67f43e6 100644 --- a/app/designSystem/landing/LandingAvatar.tsx +++ b/app/designSystem/landing/LandingAvatar.tsx @@ -11,9 +11,9 @@ interface LandingAvatarProps extends ImgHTMLAttributes { export const LandingAvatar = ({ className, src, - width = 128, - height = 128, - size = 'medium', + width = 256, + height = 256, + size = 'large', ...remainingProps }: LandingAvatarProps) => { return ( diff --git a/app/designSystem/landing/LandingCTA.tsx b/app/designSystem/landing/LandingCTA.tsx deleted file mode 100644 index 13415aa..0000000 --- a/app/designSystem/landing/LandingCTA.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { HTMLAttributes } from 'react' -import { DesignSystemUtility } from '../helpers/utility' -import LandingButton from './LandingButton' - -interface Props extends HTMLAttributes { - title?: string - subtitle?: string - buttonText?: string - buttonLink?: string -} - -export const LandingCTA: React.FC = ({ - title, - subtitle, - buttonText, - buttonLink, - className, - ...props -}) => { - return ( -
-
-
-

- {title} -

-

{subtitle}

-
- - {buttonText} - -
-
-
-
- ) -} diff --git a/app/designSystem/landing/LandingContainer.tsx b/app/designSystem/landing/LandingContainer.tsx index d586ea6..8839797 100644 --- a/app/designSystem/landing/LandingContainer.tsx +++ b/app/designSystem/landing/LandingContainer.tsx @@ -1,28 +1,17 @@ import { HTMLAttributes } from 'react' -import { LandingFooter } from './LandingFooter' -import { LandingNavBar } from './LandingNavBar/landing.navbar' interface Props extends HTMLAttributes { - navItems: { - link: string - title: string - target?: '_blank' - }[] children: React.ReactNode } export const LandingContainer: React.FC = ({ - navItems, children, ...props }) => { return (
- {children} - -
) diff --git a/app/designSystem/landing/LandingFAQ.tsx b/app/designSystem/landing/LandingFAQ.tsx deleted file mode 100644 index d091296..0000000 --- a/app/designSystem/landing/LandingFAQ.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { ArrowRightOutlined } from '@ant-design/icons' -import { HTMLAttributes } from 'react' -import { DesignSystemUtility } from '../helpers/utility' - -interface Props extends HTMLAttributes { - title: string - subtitle: string - questionAnswers: { question: string; answer: string }[] -} - -export const LandingFAQ: React.FC = ({ - title, - subtitle, - questionAnswers, - className, - ...props -}) => { - return ( -
-
-
-
-

- {title} -

-

- {subtitle} -

-
-
-
- {questionAnswers.map((item, index) => ( -
-
- - {item.question} - - - - -

- {item.answer} -

-
-
- ))} -
-
-
-
-
- ) -} diff --git a/app/designSystem/landing/LandingFeatures.tsx b/app/designSystem/landing/LandingFeatures.tsx deleted file mode 100644 index 8efc771..0000000 --- a/app/designSystem/landing/LandingFeatures.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { HTMLAttributes } from 'react' -import { DesignSystemUtility } from '../helpers/utility' - -type FeatureType = { - heading: string - description: string | any - icon: any -} - -interface Props extends HTMLAttributes { - title: string - subtitle: string - features: FeatureType[] -} - -export const LandingFeatures: React.FC = ({ - title, - subtitle, - features, - className, - ...props -}) => { - return ( -
-
-

- {title} -

-

- {subtitle} -

- -
- {features.map((item, idx) => ( -
-
- {item.icon} -
-
-

{item.heading}

{' '} -

- {item.description} -

-
-
- ))} -
-
-
- ) -} diff --git a/app/designSystem/landing/LandingFooter.tsx b/app/designSystem/landing/LandingFooter.tsx deleted file mode 100644 index 6396e3f..0000000 --- a/app/designSystem/landing/LandingFooter.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Logo } from '@/designSystem/layouts/NavigationLayout/components/Logo' -import { LinkedinFilled, TwitterCircleFilled } from '@ant-design/icons' -import { Link } from '@remix-run/react' -import { HTMLAttributes } from 'react' - -interface Props extends HTMLAttributes {} - -export const LandingFooter: React.FC = ({ ...props }) => { - const socials = [ - { - name: 'X', - icon: , - link: 'https://twitter.com/', - }, - { - name: 'LinkedIn', - icon: , - link: 'https://linkedin.com/', - }, - ] - return ( -
-
-
-
-
- -
-
Copyright © 2024
-
All rights reserved
-
-
-
- {socials.map(link => ( - - {link.name} - - ))} -
-
-
-
-
- ) -} diff --git a/app/designSystem/landing/LandingHero.tsx b/app/designSystem/landing/LandingHero.tsx deleted file mode 100644 index e2bff3e..0000000 --- a/app/designSystem/landing/LandingHero.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { HTMLAttributes, ReactNode } from 'react' -import { DesignSystemUtility } from '../helpers/utility' -import LandingButton from './LandingButton' - -interface Props extends HTMLAttributes { - title: string - subtitle: string - buttonText: string - pictureUrl?: string - socialProof?: ReactNode -} - -export const LandingHero: React.FC = ({ - title, - subtitle, - buttonText, - pictureUrl, - socialProof = '', - className, - ...props -}) => { - return ( -
-
-
-

- {title} -

-

- {subtitle} -

-
- - {buttonText} - -
- {socialProof &&
{socialProof}
} -
- -
- Landing cover -
-
-
- ) -} diff --git a/app/designSystem/landing/LandingHowItWorks.tsx b/app/designSystem/landing/LandingHowItWorks.tsx deleted file mode 100644 index 60b5cac..0000000 --- a/app/designSystem/landing/LandingHowItWorks.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { HTMLAttributes } from 'react' -import { DesignSystemUtility } from '../helpers/utility' - -type StepType = { - heading: string - description: string | any -} - -interface Props extends HTMLAttributes { - title: string - subtitle?: string - steps: StepType[] -} - -export const LandingHowItWorks: React.FC = ({ - title, - subtitle = '', - steps, - className, - ...props -}) => { - return ( -
-
-

- {title} -

-
- {steps.map((item, idx) => ( -
-
- {idx + 1} -
-
-

{item.heading}

-

{item.description}

-
-
- ))} -
-
-
- ) -} diff --git a/app/designSystem/landing/LandingNavBar/landing.desktop.navbar.tsx b/app/designSystem/landing/LandingNavBar/landing.desktop.navbar.tsx deleted file mode 100644 index 969951d..0000000 --- a/app/designSystem/landing/LandingNavBar/landing.desktop.navbar.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Logo } from '@/designSystem/layouts/NavigationLayout/components/Logo' - -import { useUserContext } from '@/core/context' -import { ArrowRightOutlined } from '@ant-design/icons' -import LandingButton from '../LandingButton' -import { LandingNavBarItem } from './landing.navbar.items' - -type Props = { - navItems: { - link: string - title: string - target?: '_blank' - }[] -} - -export const LandingDesktopNavbar = ({ navItems }: Props) => { - const { isLoggedIn } = useUserContext() - - return ( -
-
- -
- {navItems.map(item => ( - - {item.title} - - ))} -
-
-
- {isLoggedIn && ( - - Dashboard - - )} - {!isLoggedIn && ( - - Get Started - - )} -
-
- ) -} diff --git a/app/designSystem/landing/LandingNavBar/landing.mobile.navbar.tsx b/app/designSystem/landing/LandingNavBar/landing.mobile.navbar.tsx deleted file mode 100644 index a637325..0000000 --- a/app/designSystem/landing/LandingNavBar/landing.mobile.navbar.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useUserContext } from '@/core/context' -import { Logo } from '@/designSystem/layouts/NavigationLayout/components/Logo' -import { - ArrowRightOutlined, - CloseOutlined, - MenuOutlined, -} from '@ant-design/icons' -import { Link } from '@remix-run/react' -import { useState } from 'react' -import LandingButton from '../LandingButton' - -export const LandingMobileNavbar = ({ navItems }: any) => { - const { isLoggedIn } = useUserContext() - const [open, setOpen] = useState(false) - - return ( -
- - setOpen(!open)} - /> - {open && ( -
-
- -
- setOpen(!open)} - /> -
-
-
- {navItems.map((navItem: any, idx: number) => ( - <> - {navItem.children && navItem.children.length > 0 ? ( - <> - {navItem.children.map((childNavItem: any, idx: number) => ( - setOpen(false)} - className="relative max-w-[15rem] text-left text-2xl" - > - - {childNavItem.title} - - - ))} - - ) : ( - setOpen(false)} - className="relative" - > - - {navItem.title} - - - )} - - ))} -
-
- {isLoggedIn && ( - - Dashboard - - )} - {!isLoggedIn && ( - <> - - Sign Up - - - Login - - - )} -
-
- )} -
- ) -} diff --git a/app/designSystem/landing/LandingNavBar/landing.navbar.items.tsx b/app/designSystem/landing/LandingNavBar/landing.navbar.items.tsx deleted file mode 100644 index 4d99c85..0000000 --- a/app/designSystem/landing/LandingNavBar/landing.navbar.items.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Link, useLocation } from '@remix-run/react' -import { ReactNode } from 'react' -import { DesignSystemUtility } from '../../helpers/utility' - -type Props = { - href: string - children: ReactNode - active?: boolean - className?: string - target?: '_blank' -} - -export function LandingNavBarItem({ - children, - href, - active, - target, - className, -}: Props) { - const pathname = useLocation().pathname - - return ( - - {children} - - ) -} diff --git a/app/designSystem/landing/LandingNavBar/landing.navbar.tsx b/app/designSystem/landing/LandingNavBar/landing.navbar.tsx deleted file mode 100644 index dd7646d..0000000 --- a/app/designSystem/landing/LandingNavBar/landing.navbar.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useDesignSystem } from '~/designSystem/provider' -import { LandingDesktopNavbar } from './landing.desktop.navbar' -import { LandingMobileNavbar } from './landing.mobile.navbar' - -interface Props { - navItems: { - link: string - title: string - target?: '_blank' - }[] -} - -export const LandingNavBar: React.FC = ({ navItems }) => { - const { isMobile } = useDesignSystem() - - return ( -
-
- {!isMobile && } -
-
- {isMobile && } -
-
- ) -} diff --git a/app/designSystem/landing/LandingPainPoints.tsx b/app/designSystem/landing/LandingPainPoints.tsx deleted file mode 100644 index c8b0472..0000000 --- a/app/designSystem/landing/LandingPainPoints.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { ArrowDownOutlined } from '@ant-design/icons' -import { HTMLAttributes } from 'react' -import { DesignSystemUtility } from '../helpers/utility' -import RightArrow from './images/rightArrow.svg' - -type PainPointType = { - emoji: string - title: string -} - -interface Props extends HTMLAttributes { - title?: string - subtitle?: string - painPoints: PainPointType[] -} - -export const LandingPainPoints: React.FC = ({ - title, - subtitle, - painPoints, - className, - ...props -}) => { - return ( -
-
-

- {title} -

-

- {subtitle} -

- -
- {painPoints?.map((painPoint, idx) => ( - <> -
-
- {painPoint.emoji} - - {painPoint.title} - -
-
- {idx < painPoints.length - 1 && ( -
- arrow -
- )} - - ))} -
-
-
-

- there is an easier way -

-
-
-
-
- ) -} diff --git a/app/designSystem/landing/LandingPricing.tsx b/app/designSystem/landing/LandingPricing.tsx deleted file mode 100644 index b9804e5..0000000 --- a/app/designSystem/landing/LandingPricing.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { CheckCircleFilled } from '@ant-design/icons' -import { HTMLAttributes } from 'react' -import { DesignSystemUtility } from '../helpers/utility' -import LandingButton from './LandingButton' - -type Package = { - title: string - description: string - monthly: number - features: string[] - className?: string - type?: string - highlight?: boolean -} - -interface Props extends HTMLAttributes { - title: string - subtitle: string - packages: Package[] -} - -export const LandingPricing: React.FC = ({ - title, - subtitle, - packages, - className, - ...props -}) => { - return ( -
-
-
-

- {title} -

-

- {subtitle} -

-
- -
- {packages.map((item, idx) => ( - - ))} -
-
-
- ) -} - -const PricingCard = (props: Package) => { - const { title, description, monthly, features, className, type, highlight } = - props - return ( -
-
-
- {highlight && ( - - Popular - - )} - -

- {title} -

-

- $ - {monthly} - - /month - -

-
-
    - {features.map((item, idx) => ( -
  • - - {item} -
  • - ))} -
-
- - {'Get Started'} - -
-
-
- ) -} diff --git a/app/designSystem/landing/LandingRating.tsx b/app/designSystem/landing/LandingRating.tsx deleted file mode 100644 index 9e46147..0000000 --- a/app/designSystem/landing/LandingRating.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { StarFilled, StarOutlined } from '@ant-design/icons' -import clsx from 'clsx' - -/** - * Shows a rating with stars. - */ -export const LandingRating = ({ - className, - rating = 5, - maxRating = 5, - size = 'medium', -}: { - className?: string - rating?: number - maxRating?: number - size?: 'small' | 'medium' | 'large' -}) => { - return ( -
- {Array.from({ length: maxRating }).map((_, index) => { - return ( -
- { - // Return half star for half ratings - rating % 1 !== 0 && - index === Math.floor(rating) && - index + 1 === Math.ceil(rating) ? ( -
-
- ) : ( - = rating, - })} - aria-hidden="true" - /> - ) - } -
- ) - })} -
- ) -} diff --git a/app/designSystem/landing/LandingSocialProof.tsx b/app/designSystem/landing/LandingSocialProof.tsx deleted file mode 100644 index ebff1b4..0000000 --- a/app/designSystem/landing/LandingSocialProof.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { HTMLAttributes } from 'react' - -interface Props extends HTMLAttributes { - title: string -} - -export const LandingSocialProof: React.FC = () => { - const socialProofImages = [ - { url: 'https://i.imgur.com/afwBIFK.png' }, - { url: 'https://i.imgur.com/LlloOPa.png' }, - { url: 'https://i.imgur.com/j8jPb4H.png' }, - { url: 'https://i.imgur.com/mJ1sZFv.png' }, - ] - - return ( -
-
-

- Featured on -

-
- {socialProofImages.map((socialProofImage, idx) => ( - landing social logo - ))} -
-
-
- ) -} diff --git a/app/designSystem/landing/LandingSocialRating.tsx b/app/designSystem/landing/LandingSocialRating.tsx deleted file mode 100644 index 488b60b..0000000 --- a/app/designSystem/landing/LandingSocialRating.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import clsx from 'clsx' -import { LandingAvatar } from './LandingAvatar' -import { LandingRating } from './LandingRating' - -export const LandingSocialRating = ({ - children, - numberOfUsers = 100, - suffixText = 'happy users', -}: { - children?: React.ReactNode - numberOfUsers: number - suffixText?: string -}) => { - const numberText = - numberOfUsers > 1000 - ? `${(numberOfUsers / 1000).toFixed(0)}k` - : `${numberOfUsers}` - - const avatarItems = [ - { - src: 'https://randomuser.me/api/portraits/men/51.jpg', - }, - { - src: 'https://randomuser.me/api/portraits/women/9.jpg', - }, - { - src: 'https://randomuser.me/api/portraits/women/52.jpg', - }, - { - src: 'https://randomuser.me/api/portraits/men/5.jpg', - }, - { - src: 'https://randomuser.me/api/portraits/men/4.jpg', - }, - ] - return ( -
-
- {avatarItems.map((avatarItem, index) => ( - 3 ? `-ml-6` : '', - )} - /> - ))} -
- -
- - - {!children ? ( -

- {numberText}+ {suffixText} -

- ) : ( - children - )} -
-
- ) -} diff --git a/app/designSystem/landing/LandingTestimonials.tsx b/app/designSystem/landing/LandingTestimonials.tsx deleted file mode 100644 index 13e41b3..0000000 --- a/app/designSystem/landing/LandingTestimonials.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { HTMLAttributes } from 'react' -import { DesignSystemUtility } from '../helpers/utility' - -type Testimonial = { - name: string - content: string - designation?: string - avatar?: string -} - -interface Props extends HTMLAttributes { - anchorId?: string - title: string - subtitle: string - testimonials: Testimonial[] -} - -export const LandingTestimonials: React.FC = ({ - title, - subtitle, - testimonials, - className, - ...props -}) => { - return ( -
-
-
-

- {title} -

-

- {subtitle} -

-
- -
- {testimonials.map((testimonial: Testimonial, idx: number) => ( - - ))} -
-
-
- ) -} - -const TestimonialCard = ({ - name, - content, - designation, - avatar, -}: Testimonial) => { - return ( -
-
-
- - -
-

- {name} -

-

- {designation} -

-
-
- -

{content}

-
-
- ) -} diff --git a/app/designSystem/landing/images/linda.webp b/app/designSystem/landing/images/linda.webp new file mode 100644 index 0000000..3d64b06 Binary files /dev/null and b/app/designSystem/landing/images/linda.webp differ diff --git a/app/designSystem/landing/index.tsx b/app/designSystem/landing/index.tsx index 8e93622..33267f9 100644 --- a/app/designSystem/landing/index.tsx +++ b/app/designSystem/landing/index.tsx @@ -1,11 +1,3 @@ export * from './LandingContainer' -export * from './LandingCTA' -export * from './LandingFAQ' -export * from './LandingFeatures' -export * from './LandingHero' -export * from './LandingHowItWorks' -export * from './LandingPainPoints' -export * from './LandingPricing' -export * from './LandingSocialProof' -export * from './LandingSocialRating' -export * from './LandingTestimonials' +export * from './ChatHistory' +export * from './ChatBarForm' \ No newline at end of file diff --git a/app/plugins/ai/server/providers/openai/openai.provider.ts b/app/plugins/ai/server/providers/openai/openai.provider.ts index cd67404..dcffd8f 100644 --- a/app/plugins/ai/server/providers/openai/openai.provider.ts +++ b/app/plugins/ai/server/providers/openai/openai.provider.ts @@ -1,9 +1,13 @@ +import { response } from 'express' import { ReadStream } from 'fs' import OpenaiSDK from 'openai' import { zodResponseFormat } from 'openai/helpers/zod' import { ParsedChatCompletion } from 'openai/resources/beta/chat/completions' import { z, ZodType } from 'zod' +type Message = OpenaiSDK.Chat.Completions.ChatCompletionMessageParam +type Response = OpenaiSDK.Chat.Completions.ChatCompletion + export type OpenaiGenerateTextOptions = { prompt: string attachmentUrls?: string[] @@ -22,7 +26,7 @@ enum OpenaiModel { type BuildMessageOptions = { content: string attachmentUrls?: string[] - history?: string[] + history?: Message[] context?: string } @@ -59,7 +63,61 @@ export class OpenaiProvider { } async generateText(options: OpenaiGenerateTextOptions): Promise { - return + const { + prompt, + attachmentUrls, + history, + context, + } = options + + const messages : Message[] = this.buildMessages(); + const response : Response = await this.createResponse(options, messages); + const content = response.choices[0].message?.content as string + return content; + } + + private buildMessages(options?: BuildMessageOptions): Message[] { + const { content, context, history } = options || {}; + const messages: Message[] = []; + + const promptSystem: Message = { + role: 'system', + content: `${context}`.trim(), + } + + const guideline: Message = { + role: 'system', + content: ` + If the user request a pdf file, you should answer with 'Here is the pdf!', + and then with the potential content of the pdf encapsulated with . + ` + } + + messages.push(promptSystem as Message); + messages.push(guideline as Message); + if(history){ messages.push(...history) } + if(content) { + messages.push({ + role: 'user', + content: [ {type:'text', text: `${content}`.trim()} ] + } as Message) + } + + return messages + } + + private createResponse(options: OpenaiGenerateTextOptions, messages: Message[]): Promise { + const { prompt } = options + return this.api.chat.completions.create({ + model: OpenaiModel.DEFAULT, + messages: [ + ...messages, + { + role: 'user', + content: `${prompt}`.trim(), + } as Message, + ] + }) } async generateJson< diff --git a/app/routes/_auth.login_/route.tsx b/app/routes/_auth.login_/route.tsx deleted file mode 100644 index 199bc4e..0000000 --- a/app/routes/_auth.login_/route.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { Api } from '@/core/trpc' -import { AppHeader } from '@/designSystem/ui/AppHeader' -import { useNavigate, useSearchParams } from '@remix-run/react' -import { Button, Flex, Form, Input, Typography } from 'antd' -import { useEffect, useState } from 'react' -import { AuthenticationClient } from '~/core/authentication/client' - -export default function LoginPage() { - const router = useNavigate() - const [searchParams] = useSearchParams() - - const [form] = Form.useForm() - const [isLoading, setLoading] = useState(false) - - const { mutateAsync: login } = Api.authentication.login.useMutation({ - onSuccess: data => { - if (data.redirect) { - window.location.href = data.redirect - } - }, - }) - - const errorKey = searchParams.get('error') - - const errorMessage = { - Signin: 'Try signing in with a different account.', - OAuthSignin: 'Try signing in with a different account.', - OAuthCallback: 'Try signing in with a different account.', - OAuthCreateAccount: 'Try signing in with a different account.', - EmailCreateAccount: 'Try signing in with a different account.', - Callback: 'Try signing in with a different account.', - OAuthAccountNotLinked: - 'To confirm your identity, sign in with the same account you used originally.', - EmailSignin: 'Check your email address.', - CredentialsSignin: - 'Sign in failed. Check the details you provided are correct.', - default: 'Unable to sign in.', - }[errorKey ?? 'default'] - - useEffect(() => { - if (process.env.NODE_ENV === 'development') { - form.setFieldValue('email', 'test@test.com') - form.setFieldValue('password', 'password') - } - }, []) - - const handleSubmit = async (values: any) => { - setLoading(true) - - try { - await login({ email: values.email, password: values.password }) - } catch (error) { - console.error(`Could not login: ${error.message}`, { variant: 'error' }) - - setLoading(false) - } - } - - return ( - - - - - {errorKey && ( - {errorMessage} - )} - -
- - - - - - - - - - - - - - - - - -
- - - - -
-
- ) -} diff --git a/app/routes/_auth.register_/route.tsx b/app/routes/_auth.register_/route.tsx deleted file mode 100644 index 8a94b8e..0000000 --- a/app/routes/_auth.register_/route.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Utility } from '@/core/helpers/utility' -import { Api } from '@/core/trpc' -import { AppHeader } from '@/designSystem/ui/AppHeader' -import { User } from '@prisma/client' -import { useNavigate, useSearchParams } from '@remix-run/react' -import { Button, Flex, Form, Input, Typography } from 'antd' -import { useEffect, useState } from 'react' - -export default function RegisterPage() { - const router = useNavigate() - const [searchParams] = useSearchParams() - - const [form] = Form.useForm() - - const [isLoading, setLoading] = useState(false) - - const { mutateAsync: register } = Api.authentication.register.useMutation() - - const { mutateAsync: login } = Api.authentication.login.useMutation({ - onSuccess: data => { - if (data.redirect) { - window.location.href = data.redirect - } - }, - }) - - useEffect(() => { - const email = searchParams.get('email')?.trim() - - if (Utility.isDefined(email)) { - form.setFieldsValue({ email }) - } - }, [searchParams]) - - const handleSubmit = async (values: Partial) => { - setLoading(true) - - try { - const tokenInvitation = searchParams.get('tokenInvitation') ?? undefined - - await register({ ...values, tokenInvitation }) - - login(values) - } catch (error) { - console.error(`Could not signup: ${error.message}`, { - variant: 'error', - }) - - setLoading(false) - } - } - - return ( - - - - -
- - - - - - - - - - - - - - -
- - -
-
- ) -} diff --git a/app/routes/_auth.reset-password.$token_/route.tsx b/app/routes/_auth.reset-password.$token_/route.tsx deleted file mode 100644 index 03dc591..0000000 --- a/app/routes/_auth.reset-password.$token_/route.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Api } from '@/core/trpc' -import { AppHeader } from '@/designSystem' -import { useNavigate, useParams } from '@remix-run/react' -import { Alert, Button, Flex, Form, Input, message, Typography } from 'antd' - -const { Text } = Typography - -export default function ResetPasswordTokenPage() { - const router = useNavigate() - - const { token } = useParams<{ token: string }>() - - const [form] = Form.useForm() - - const { - mutateAsync: resetPassword, - isLoading, - isSuccess, - } = Api.authentication.resetPassword.useMutation() - - const handleSubmit = async (values: any) => { - try { - await resetPassword({ token, password: values.password }) - } catch (error) { - message.error(`Could not reset password: ${error.message}`) - } - } - - return ( - <> - - - - - {isSuccess && ( - - )} - - {!isSuccess && ( -
- - - - - - - - - - -
- )} - - - - - or - - - -
-
- - ) -} diff --git a/app/routes/_auth.reset-password_/route.tsx b/app/routes/_auth.reset-password_/route.tsx deleted file mode 100644 index f02b5d5..0000000 --- a/app/routes/_auth.reset-password_/route.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { Api } from '@/core/trpc' -import { AppHeader } from '@/designSystem/ui/AppHeader' -import { useNavigate } from '@remix-run/react' -import { Alert, Button, Flex, Form, Input, message, Typography } from 'antd' -import { useState } from 'react' - -const { Text } = Typography - -export default function ResetPasswordPage() { - const navigate = useNavigate() - - const [email, setEmail] = useState() - - const [form] = Form.useForm() - - const { - mutateAsync: resetPassword, - isLoading, - isSuccess, - } = Api.authentication.sendResetPasswordEmail.useMutation() - - const handleSubmit = async (values: any) => { - try { - setEmail(values.email) - - await resetPassword({ email: values.email }) - } catch (error) { - message.error(`Could not reset password: ${error}`) - } - } - - return ( - - - - - {isSuccess && ( - - )} - - {!isSuccess && ( - <> -
- - - - - - -
- - )} - - - - - or - - - -
-
- ) -} diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index fc3fc19..a57881f 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,232 +1,39 @@ +import { useState } from 'react' import { LandingContainer, - LandingCTA, - LandingFAQ, - LandingFeatures, - LandingHero, - LandingHowItWorks, - LandingPainPoints, - LandingPricing, - LandingSocialProof, - LandingSocialRating, - LandingTestimonials, + ChatHistory, + ChatBarForm } from '~/designSystem' -export default function LandingPage() { - const features = [ - { - heading: `Smart Task Management`, - description: `Organize and prioritize tasks with intuitive drag-and-drop interfaces and automated workflows.`, - icon: , - }, - { - heading: `Real-Time Collaboration`, - description: `Keep your team aligned with instant updates, comments, and file sharing in one central hub.`, - icon: , - }, - { - heading: `Progress Tracking`, - description: `Get a clear view of project status with visual dashboards, timelines and progress indicators.`, - icon: , - }, - { - heading: `Deadline Management`, - description: `Never miss a deadline with automated reminders and calendar integrations.`, - icon: , - }, - { - heading: `Resource Planning`, - description: `Optimize team workload and resource allocation with capacity planning tools.`, - icon: , - }, - { - heading: `Performance Analytics`, - description: `Make data-driven decisions with detailed project analytics and team performance metrics.`, - icon: , - }, - ] - - const testimonials = [ - { - name: `Sarah Chen`, - designation: `Product Manager at TechFlow`, - content: `Since implementing this platform, our team productivity increased by 45%. Tasks that used to fall through the cracks are now completed on time.`, - avatar: 'https://randomuser.me/api/portraits/women/6.jpg', - }, - { - name: `Mark Thompson`, - designation: `CEO at GrowthLabs`, - content: `The visibility this tool provides has transformed how we manage projects. We've cut meeting time by 60% and improved delivery times.`, - avatar: 'https://randomuser.me/api/portraits/men/7.jpg', - }, - { - name: `Lisa Rodriguez`, - designation: `Team Lead at InnovateCo`, - content: `Finally, a project management tool that's both powerful and easy to use. Our team adopted it within days, not weeks.`, - avatar: 'https://randomuser.me/api/portraits/women/27.jpg', - }, - ] - - const navItems = [ - { - title: `Features`, - link: `#features`, - }, - { - title: `Pricing`, - link: `#pricing`, - }, - { - title: `FAQ`, - link: `#faq`, - }, - ] - - const packages = [ - { - title: `Starter`, - description: `Perfect for small teams getting started`, - monthly: 29, - yearly: 290, - features: [ - `Up to 10 team members`, - `Basic task management`, - `File sharing`, - `Email support`, - ], - }, - { - title: `Professional`, - description: `Ideal for growing businesses`, - monthly: 79, - yearly: 790, - features: [ - `Up to 50 team members`, - `Advanced workflows`, - `Analytics dashboard`, - `Priority support`, - ], - highlight: true, - }, - { - title: `Enterprise`, - description: `For large organizations`, - monthly: 199, - yearly: 1990, - features: [ - `Unlimited team members`, - `Custom integrations`, - `Dedicated success manager`, - `24/7 support`, - ], - }, - ] - - const questionAnswers = [ - { - question: `How quickly can we get started?`, - answer: `You can be up and running in minutes. Our intuitive interface requires minimal training, and we provide comprehensive onboarding resources.`, - }, - { - question: `Can I integrate with other tools?`, - answer: `Yes, we integrate seamlessly with popular tools like Slack, Google Workspace, and Microsoft Office 365.`, - }, - { - question: `What kind of support do you offer?`, - answer: `We provide email support for all plans, with priority support and dedicated success managers for higher tiers.`, - }, - { - question: `Is my data secure?`, - answer: `We use enterprise-grade encryption and regular security audits to ensure your data is always protected.`, - }, - ] +type ChatMessage = { + ai: boolean + text: string +} - const steps = [ - { - heading: `Sign Up`, - description: `Create your account in minutes and invite your team members.`, - }, - { - heading: `Import Projects`, - description: `Easily import existing projects or start fresh with our templates.`, - }, - { - heading: `Customize Workflow`, - description: `Set up workflows that match your team's unique processes.`, - }, - { - heading: `Track Progress`, - description: `Monitor progress and celebrate team achievements.`, - }, - ] +export default function LandingPage() { - const painPoints = [ - { - emoji: `😫`, - title: `Drowning in endless email threads and scattered communications`, - }, - { - emoji: `😤`, - title: `Missing deadlines due to poor task visibility`, - }, - { - emoji: `😩`, - title: `Wasting time in unnecessary status update meetings`, - }, - ] + const handleNewMessage = (newMessage: ChatMessage) => { + console.log('New message:', newMessage); + setMessages((chats) => { + chats.push(newMessage) + return [...chats] + }); + console.log('Updated messages:', messages); + document.getElementById('chat-history')?.scrollTo(0, document.getElementById('chat-history')?.scrollHeight || 0); + // Here you can add the logic to send the message to your AI service and update the chat history with the response. + } + + const [messages, setMessages] = useState([ + { ai: true, text: 'Hello! How can I assist you today?' } + ]) return ( - - - } - /> - - - - - - - - + + ) } diff --git a/app/routes/_logged.home_/route.tsx b/app/routes/_logged.home_/route.tsx deleted file mode 100644 index f36bf88..0000000 --- a/app/routes/_logged.home_/route.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Typography, Row, Col, Card } from 'antd' -const { Title, Paragraph } = Typography -import { useUserContext } from '@/core/context' -import dayjs from 'dayjs' -import { useLocation, useNavigate, useParams } from '@remix-run/react' -import { useUploadPublic } from '@/plugins/upload/client' -import { Api } from '@/core/trpc' -import { PageLayout } from '@/designSystem' - -export default function HomePage() { - const { user } = useUserContext() - - return ( - - - -
- - <i className="las la-home" style={{ marginRight: '0.5rem' }}></i> - Welcome to Our Platform - - - Your one-stop solution for managing organizations and connecting - with others - -
- - - - - - - Personal Space - - {user ? ( - <>Welcome back, {user.name || 'Guest'}! - ) : ( - <>Sign in to access your personal dashboard - )} - - - - - - - - Organizations - - Manage your organizations and collaborate with team members - - - - - - - - Communication - - Stay connected with your team through our integrated chat - system - - - - - - - - Analytics - - Track your progress and monitor important metrics - - - - - - -
-
- ) -} diff --git a/app/routes/_logged.organizations.$organizationId.home_/route.tsx b/app/routes/_logged.organizations.$organizationId.home_/route.tsx deleted file mode 100644 index 8bd202d..0000000 --- a/app/routes/_logged.organizations.$organizationId.home_/route.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { Typography, Card, Space, Row, Col } from 'antd' -const { Title, Text } = Typography -import { useUserContext } from '@/core/context' -import dayjs from 'dayjs' -import { useLocation, useNavigate, useParams } from '@remix-run/react' -import { useUploadPublic } from '@/plugins/upload/client' -import { Api } from '@/core/trpc' -import { PageLayout } from '@/designSystem' - -export default function HomePage() { - const { user, organization } = useUserContext() - - return ( - -
- - {/* Welcome Section */} -
- - <i className="las la-home" style={{ marginRight: '12px' }}></i> - Welcome to {organization?.name} - - - Your digital workspace for collaboration and productivity - -
- - {/* Info Cards */} - - - - -
- } - > - - Name: {user?.name} - Email: {user?.email} - - Member since:{' '} - {dayjs(user?.createdAt).format('MMMM D, YYYY')} - - - } - /> - - - - - - - - } - > - - Name: {organization?.name} - - Created:{' '} - {dayjs(organization?.createdAt).format('MMMM D, YYYY')} - - - } - /> - - - - - - - - } - > - - - View Dashboard - - - Team Members - - - Settings - - - } - /> - - - - - -
- ) -} diff --git a/app/routes/_logged.organizations.$organizationId.members_/hooks/useDelete.tsx b/app/routes/_logged.organizations.$organizationId.members_/hooks/useDelete.tsx deleted file mode 100644 index 1ae179a..0000000 --- a/app/routes/_logged.organizations.$organizationId.members_/hooks/useDelete.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useUserContext } from '@/core/context' -import { Api } from '@/core/trpc' -import { OrganizationRole, User } from '@prisma/client' -import { useState } from 'react' -import { useDesignSystem } from '~/designSystem' - -type UserWithOrganizationRoles = User & { - organizationRoles: OrganizationRole[] -} - -type Props = { users: UserWithOrganizationRoles[] } - -export const useDelete = ({ users }: Props) => { - const { message } = useDesignSystem() - - const { user: userLogged, checkOrganizationRole } = useUserContext() - - const [isLoading, setLoading] = useState(false) - - const countOwners = users.filter(user => - user.organizationRoles.find(role => role.name === 'owner'), - ).length - - const { mutateAsync: deleteOrganizationRole } = - Api.organizationRole.deleteMany.useMutation() - - const canDeleteUser = (user: UserWithOrganizationRoles) => { - const isOwner = user.organizationRoles.find( - organizationRole => organizationRole.name === 'owner', - ) - - const isSelf = userLogged.id === user.id - - if (isSelf) { - return !isOwner || countOwners > 1 - } else { - return checkOrganizationRole('owner') - } - } - - const deleteUser = async (user: UserWithOrganizationRoles) => { - setLoading(true) - - let isSuccess = false - - if (canDeleteUser(user)) { - try { - await deleteOrganizationRole({ - where: { - userId: user.id, - }, - }) - - isSuccess = true - - if (user.id === userLogged.id) { - window.location.replace('/') - } else { - message.info(`${user.email} has been removed`) - } - } catch (error) { - message.error(error.message) - } - } - - setLoading(false) - - return isSuccess - } - - return { - deleteUser, - isLoadingDelete: isLoading, - canDeleteUser, - } -} diff --git a/app/routes/_logged.organizations.$organizationId.members_/hooks/useInvitation.tsx b/app/routes/_logged.organizations.$organizationId.members_/hooks/useInvitation.tsx deleted file mode 100644 index 8e8902f..0000000 --- a/app/routes/_logged.organizations.$organizationId.members_/hooks/useInvitation.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Utility } from '@/core/helpers/utility' -import { Api } from '@/core/trpc' -import { Organization } from '@prisma/client' -import { useState } from 'react' -import { useDesignSystem } from '~/designSystem' - -type Props = { - email: string - organization: Organization -} - -export const useInvitation = ({ organization, email }: Props) => { - const { message } = useDesignSystem() - - const [isLoading, setLoading] = useState(false) - - const emailEnsured = email?.trim()?.toLowerCase() - - const { refetch: fetchOrganizationRole } = - Api.organizationRole.findFirst.useQuery( - { - where: { - organizationId: organization.id, - user: { - email: emailEnsured, - }, - }, - }, - { enabled: false }, - ) - - const { mutateAsync: inviteToOrganization } = - Api.authentication.inviteToOrganization.useMutation() - - const invite = async () => { - setLoading(true) - - let isSuccess = false - - if (Utility.isDefined(email)) { - try { - const organizationRole = await fetchOrganizationRole().then( - response => response.data, - ) - - if (!organizationRole) { - await inviteToOrganization({ - organizationId: organization.id, - email: emailEnsured, - }) - - message.info(`${emailEnsured} has been added`) - } - - isSuccess = true - } catch (error) { - message.error(error.message) - } - } - - setLoading(false) - - return isSuccess - } - - return { - invite, - isLoadingInvitation: isLoading, - } -} diff --git a/app/routes/_logged.organizations.$organizationId.members_/hooks/useUpdate.tsx b/app/routes/_logged.organizations.$organizationId.members_/hooks/useUpdate.tsx deleted file mode 100644 index 5f33244..0000000 --- a/app/routes/_logged.organizations.$organizationId.members_/hooks/useUpdate.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useUserContext } from '@/core/context' -import { Api } from '@/core/trpc' -import { useDesignSystem } from '@/designSystem' -import { OrganizationRole, User } from '@prisma/client' -import { useState } from 'react' - -type UserWithOrganizationRoles = User & { - organizationRoles: OrganizationRole[] -} - -type Props = { users: UserWithOrganizationRoles[] } - -export const useUpdate = ({ users }: Props) => { - const { checkOrganizationRole } = useUserContext() - const { message } = useDesignSystem() - - const [isLoading, setLoading] = useState(false) - - const countOwners = users.filter(user => - user.organizationRoles.find(role => role.name === 'owner'), - ).length - - const { mutateAsync: updateOrganizationRole } = - Api.organizationRole.update.useMutation() - - const canUpdate = () => checkOrganizationRole('owner') - - const update = async (organizationRole: OrganizationRole, name: string) => { - setLoading(true) - - let isSuccess = false - - if (canUpdate()) { - try { - const isDowngrade = - organizationRole.name === 'owner' && name !== 'owner' - - if (isDowngrade && countOwners < 2) { - throw new Error(`There must be at least 1 owner`) - } - - await updateOrganizationRole({ - where: { id: organizationRole.id }, - data: { name }, - }) - - isSuccess = true - - message.info(`Role has been updated`) - } catch (error) { - message.error(error.message) - } - } - - setLoading(false) - - return isSuccess - } - - return { - update, - canUpdate, - isLoadingUpdate: isLoading, - } -} diff --git a/app/routes/_logged.organizations.$organizationId.members_/route.tsx b/app/routes/_logged.organizations.$organizationId.members_/route.tsx deleted file mode 100644 index 7c4bbcb..0000000 --- a/app/routes/_logged.organizations.$organizationId.members_/route.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import { - Avatar, - Button, - Dropdown, - Flex, - Form, - Input, - MenuProps, - Popconfirm, - Row, - Table, - TableProps, - Tag, - Tooltip, - Typography, -} from 'antd' - -import { useUserContext } from '@/core/context' -import { Utility } from '@/core/helpers/utility' -import { Api } from '@/core/trpc' -import { PageLayout, useDesignSystem } from '@/designSystem' -import { - CheckOutlined, - CopyOutlined, - DeleteOutlined, - EditFilled, - HourglassOutlined, - UserAddOutlined, -} from '@ant-design/icons' -import { OrganizationRole, User } from '@prisma/client' -import { useState } from 'react' -import { useDelete } from './hooks/useDelete' -import { useInvitation } from './hooks/useInvitation' -import { useUpdate } from './hooks/useUpdate' - -type UserWithOrganizationRoles = User & { - organizationRoles: OrganizationRole[] -} - -export default function OrganizationTeamPage() { - const { organization } = useUserContext() - const { message } = useDesignSystem() - - const [form] = Form.useForm() - - const [email, setEmail] = useState('') - - const { - data: users, - isLoading: isLoadingUsers, - refetch: refetchUsers, - } = Api.user.findMany.useQuery( - { - where: { - organizationRoles: { some: { organizationId: organization.id } }, - }, - include: { - organizationRoles: { where: { organizationId: organization.id } }, - }, - orderBy: { createdAt: 'desc' }, - }, - { initialData: [] }, - ) - - const { invite, isLoadingInvitation } = useInvitation({ organization, email }) - - const { update, canUpdate, isLoadingUpdate } = useUpdate({ users }) - - const { deleteUser, canDeleteUser, isLoadingDelete } = useDelete({ users }) - - const handleInvite = async () => { - const isSuccess = await invite() - - if (isSuccess) { - setEmail('') - form.setFieldsValue({ email: '' }) - refetchUsers() - } - } - - const handleUpdate = async ( - organizationRole: OrganizationRole, - name: string, - ) => { - const isSuccess = await update(organizationRole, name) - - if (isSuccess) { - refetchUsers() - } - } - - const handleDeleteUser = async (user: UserWithOrganizationRoles) => { - const isSuccess = await deleteUser(user) - - if (isSuccess) { - refetchUsers() - } - } - - const dataSource = users - .map(item => ({ ...item, key: item.id })) - .filter(item => { - const search = email.trim().toLowerCase() - const userEmail = item.email.trim().toLowerCase() - const userName = item.name?.trim().toLowerCase() ?? '' - - return userEmail.includes(search) || userName.includes(search) - }) - - const columns: TableProps['columns'] = [ - { - title: `${dataSource.length} Members`, - key: 'id', - render: (user: UserWithOrganizationRoles) => ( - - - {Utility.stringToInitials(user.name ?? user.email)} - - - - {user.name} - - {user.email} - - - - ), - }, - { - title: 'Role', - key: 'role', - render: (user: UserWithOrganizationRoles) => ( - - {user.organizationRoles.map(role => { - const isInvited = user.status === 'INVITED' - - const isUserOwner = role.name === 'owner' - - const itemsDropdown: MenuProps['items'] = [ - { - key: 'owner', - label: ( - Owner {isUserOwner && } - ), - onClick: () => handleUpdate(role, 'owner'), - }, - { - key: 'member', - label: ( - - Member {!isUserOwner && } - - ), - onClick: () => handleUpdate(role, 'member'), - }, - ] - - return ( - - - {isInvited && } - - {role.name} - - {canUpdate() && ( - - - - )} - - - ) - })} - - ), - }, - { - title: '', - key: 'invite', - width: '50px', - render: (user: UserWithOrganizationRoles) => { - return ( - - {user.status === 'INVITED' && ( - - - } - /> - - - - - - - ) -} diff --git a/app/routes/_logged.organizations.$organizationId.pricing_/route.tsx b/app/routes/_logged.organizations.$organizationId.pricing_/route.tsx deleted file mode 100644 index e252d55..0000000 --- a/app/routes/_logged.organizations.$organizationId.pricing_/route.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { Card, Col, Empty, Flex, Row, Spin, Tag, Typography } from 'antd' - -import { Api } from '@/core/trpc' -import { PageLayout } from '@/designSystem' -import { Product } from '@/plugins/payment/server' -import { useParams } from '@remix-run/react' - -export default function OrganizationPricingPage() { - const params = useParams<{ organizationId: string }>() - - const { data: products, isLoading: isLoadingProducts } = - Api.billing.findManyProducts.useQuery({}, { initialData: [] }) - - const { mutateAsync: createPaymentLink } = - Api.billing.createPaymentLink.useMutation() - - const { data: subscriptions } = Api.billing.findManySubscriptions.useQuery( - { - organizationId: params.organizationId, - }, - { initialData: [] }, - ) - - const handleClick = async (product: Product) => { - const { url } = await createPaymentLink({ - productId: product.id, - organizationId: params.organizationId, - }) - - window.open(url, '_blank') - } - - const getPrice = (product: Product) => { - if (product.price === 0) { - return 'Free' - } - - const mapping = { - usd: '${{price}}', - } - - const pattern = mapping[product.currency] - - if (pattern) { - return pattern.replace('{{price}}', product.price) - } - - return `${product.currency.toUpperCase()} ${product.price}` - } - - const isSubscribed = (product: Product) => { - return subscriptions.find( - subscription => subscription.productId === product.id, - ) - } - - return ( - - - {products.length === 0 && isLoadingProducts && } - - {products.length === 0 && !isLoadingProducts && ( - - )} - - {products.map(product => ( - - handleClick(product)} - cover={ - - {product.name} - - } - > - - - {product.name} - - - - - {getPrice(product)} - - {product.interval && ( - - / {product.interval} - - )} - - - {isSubscribed(product) && ( -
- Active -
- )} - - - {product.description} - -
-
- - ))} - - - ) -} diff --git a/app/routes/_logged.organizations.$organizationId.settings_/route.tsx b/app/routes/_logged.organizations.$organizationId.settings_/route.tsx deleted file mode 100644 index f088a17..0000000 --- a/app/routes/_logged.organizations.$organizationId.settings_/route.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { Avatar, Button, Flex, Form, Input, message, Popconfirm } from 'antd' - -import { useUserContext } from '@/core/context' -import { Utility } from '@/core/helpers/utility' -import { Api } from '@/core/trpc' -import { PageLayout } from '@/designSystem' -import { User } from '@prisma/client' -import { useEffect } from 'react' - -export default function OrganizationSettingsPage() { - const { - organization, - refetchOrganization, - refetchOrganizations, - checkOrganizationRole, - } = useUserContext() - - const { mutateAsync: updateOrganization, isLoading: isLoadingUpdate } = - Api.organization.update.useMutation() - - const { mutateAsync: deleteOrganization, isLoading: isLoadingDelete } = - Api.organization.delete.useMutation() - - const [form] = Form.useForm() - - useEffect(() => { - form.setFieldsValue(organization) - }, [organization]) - - const handleSubmit = async (values: Partial) => { - try { - await updateOrganization({ - where: { id: organization.id }, - data: { - name: values.name, - pictureUrl: values.pictureUrl, - }, - }) - - refetchOrganization() - refetchOrganizations() - } catch (error) { - message.error(`Could not save user: ${error.message}`) - } - } - - const handleClickDelete = async () => { - await deleteOrganization({ where: { id: organization.id } }) - - window.location.replace('/home') - } - - return ( - - - - {Utility.stringToInitials(organization.name)[0]} - - - -
- - - - - - - - - - - - - - - - {checkOrganizationRole('owner') && ( - - - - - - )} -
- ) -} diff --git a/app/routes/_logged.organizations/route.tsx b/app/routes/_logged.organizations/route.tsx deleted file mode 100644 index e4a2646..0000000 --- a/app/routes/_logged.organizations/route.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useUserContext } from '@/core/context' -import { MrbSplashScreen } from '@/designSystem' -import { Outlet } from '@remix-run/react' -import { useEffect } from 'react' - -export default function OrganizationsLayout() { - const { isLoadingOrganization, organization } = useUserContext() - - useEffect(() => { - if (!isLoadingOrganization && !organization) { - window.location.replace('/') - } - }, [isLoadingOrganization, organization]) - - if (isLoadingOrganization) { - return - } - - if (organization) { - return - } -} diff --git a/app/routes/_logged.pricing_/route.tsx b/app/routes/_logged.pricing_/route.tsx deleted file mode 100644 index 1612944..0000000 --- a/app/routes/_logged.pricing_/route.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Card, Col, Empty, Flex, Row, Spin, Tag, Typography } from 'antd' - -import { Api } from '@/core/trpc' -import { PageLayout } from '@/designSystem' -import { Product } from '~/plugins/payment/server' - -export default function PricingPage() { - const { data: products, isLoading: isLoadingProducts } = - Api.billing.findManyProducts.useQuery({}, { initialData: [] }) - - const { mutateAsync: createPaymentLink } = - Api.billing.createPaymentLink.useMutation() - - const { data: subscriptions } = Api.billing.findManySubscriptions.useQuery( - {}, - { initialData: [] }, - ) - - const handleClick = async (product: Product) => { - const { url } = await createPaymentLink({ productId: product.id }) - - window.open(url, '_blank') - } - - const getPrice = (product: Product) => { - if (product.price === 0) { - return 'Free' - } - - const mapping = { - usd: '${{price}}', - } - - const pattern = mapping[product.currency] - - if (pattern) { - return pattern.replace('{{price}}', product.price) - } - - return `${product.currency.toUpperCase()} ${product.price}` - } - - const isSubscribed = (product: Product) => { - return subscriptions.find( - subscription => subscription.productId === product.id, - ) - } - - return ( - - - {products.length === 0 && isLoadingProducts && } - - {products.length === 0 && !isLoadingProducts && ( - - )} - - {products.map(product => ( -
- handleClick(product)} - cover={ - - {product.name} - - } - > - - - {product.name} - - - - - {getPrice(product)} - - {product.interval && ( - - / {product.interval} - - )} - - - {isSubscribed(product) && ( -
- Active -
- )} - - - {product.description} - -
-
- - ))} - - - ) -} diff --git a/app/routes/_logged.profile_/route.tsx b/app/routes/_logged.profile_/route.tsx deleted file mode 100644 index 7801464..0000000 --- a/app/routes/_logged.profile_/route.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Avatar, Button, Flex, Form, Input, Typography } from 'antd' - -import { useUserContext } from '@/core/context' -import { Utility } from '@/core/helpers/utility' -import { Api } from '@/core/trpc' -import { PageLayout } from '@/designSystem' -import { User } from '@prisma/client' -import { useEffect, useState } from 'react' - -export default function ProfilePage() { - const { user, refetch: refetchUser } = useUserContext() - const { mutateAsync: logout } = Api.authentication.logout.useMutation({ - onSuccess: data => { - if (data.redirect) { - window.location.href = data.redirect - } - }, - }) - - const [form] = Form.useForm() - - const [isLoading, setLoading] = useState(false) - const [isLoadingLogout, setLoadingLogout] = useState(false) - - const { mutateAsync: updateUser } = Api.user.update.useMutation() - - useEffect(() => { - form.setFieldsValue(user) - }, [user]) - - const handleSubmit = async (values: Partial) => { - setLoading(true) - - try { - await updateUser({ - where: { id: user.id }, - data: { - email: values.email, - name: values.name, - pictureUrl: values.pictureUrl, - }, - }) - - refetchUser() - } catch (error) { - console.error(`Could not save user: ${error.message}`, { - variant: 'error', - }) - } - - setLoading(false) - } - - const handleClickLogout = async () => { - setLoadingLogout(true) - - try { - await logout() - } catch (error) { - console.error(`Could not logout: ${error.message}`, { - variant: 'error', - }) - - setLoadingLogout(false) - } - } - - return ( - - - Profile - - - - - - {Utility.stringToInitials(user?.name)} - - - -
- - - - - - - - - - - - - - - - - - -
- ) -} diff --git a/app/routes/_logged.tsx b/app/routes/_logged.tsx deleted file mode 100644 index d0b0799..0000000 --- a/app/routes/_logged.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useUserContext } from '@/core/context' -import { NavigationLayout } from '@/designSystem/layouts/NavigationLayout' -import { Outlet, useNavigate } from '@remix-run/react' -import { useEffect } from 'react' -import { MrbSplashScreen } from '~/designSystem' - -export default function LoggedLayout() { - const { isLoggedIn, isLoading } = useUserContext() - - const router = useNavigate() - - useEffect(() => { - if (!isLoading && !isLoggedIn) { - router('/login') - } - }, [isLoading, isLoggedIn]) - - if (isLoading) { - return - } - - if (isLoggedIn) { - return ( - - - - ) - } -} diff --git a/app/routes/hooks/handleChat.tsx b/app/routes/hooks/handleChat.tsx new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 2663d01..5dfd143 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "isbot": "^4.1.0", "jsonwebtoken": "8.5.1", "jsonwebtoken-esm": "1.0.5", + "jspdf": "^4.1.0", "morgan": "^1.10.0", "node-cron": "^3.0.3", "node-mailjet": "^6.0.6", @@ -89,6 +90,7 @@ "@types/cookie": "^1.0.0", "@types/express": "^5.0.0", "@types/jsonwebtoken": "^9.0.7", + "@types/jspdf": "^2.0.0", "@types/node-cron": "^3.0.11", "@types/nodemailer": "^6.4.16", "@types/passport": "^1.0.16", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b95ebc..bc9b5a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: jsonwebtoken-esm: specifier: 1.0.5 version: 1.0.5 + jspdf: + specifier: ^4.1.0 + version: 4.1.0 morgan: specifier: ^1.10.0 version: 1.10.0 @@ -192,6 +195,9 @@ importers: '@types/jsonwebtoken': specifier: ^9.0.7 version: 9.0.7 + '@types/jspdf': + specifier: ^2.0.0 + version: 2.0.0 '@types/node-cron': specifier: ^3.0.11 version: 3.0.11 @@ -590,6 +596,10 @@ packages: resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.25.0': resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} engines: {node: '>=6.9.0'} @@ -1252,67 +1262,79 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -1616,46 +1638,55 @@ packages: resolution: {integrity: sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.22.4': resolution: {integrity: sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.22.4': resolution: {integrity: sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.22.4': resolution: {integrity: sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.22.4': resolution: {integrity: sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.22.4': resolution: {integrity: sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.22.4': resolution: {integrity: sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.22.4': resolution: {integrity: sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.22.4': resolution: {integrity: sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.22.4': resolution: {integrity: sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==} @@ -1992,6 +2023,10 @@ packages: '@types/jsonwebtoken@9.0.7': resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + '@types/jspdf@2.0.0': + resolution: {integrity: sha512-oonYDXI4GegGaG7FFVtriJ+Yqlh4YR3L3NVDiwCEBVG7sbya19SoGx4MW4kg1MCMRPgkbbFTck8YKJL8PrkDfA==} + deprecated: This is a stub types definition. jspdf provides its own type definitions, so you do not need this installed. + '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -2022,6 +2057,9 @@ packages: '@types/nodemailer@6.4.16': resolution: {integrity: sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==} + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + '@types/passport@1.0.16': resolution: {integrity: sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==} @@ -2031,6 +2069,9 @@ packages: '@types/qs@6.9.16': resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} @@ -2049,6 +2090,9 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2299,6 +2343,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2397,6 +2445,10 @@ packages: caniuse-lite@1.0.30001663: resolution: {integrity: sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -2573,6 +2625,9 @@ packages: copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + core-js@3.48.0: + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2580,6 +2635,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} @@ -2760,6 +2818,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -3049,6 +3110,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + fast-xml-parser@4.4.1: resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} hasBin: true @@ -3065,6 +3129,9 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -3290,6 +3357,10 @@ packages: resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -3355,6 +3426,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -3614,6 +3688,9 @@ packages: resolution: {integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==} engines: {node: '>=4', npm: '>=1.4.28'} + jspdf@4.1.0: + resolution: {integrity: sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -4158,6 +4235,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + papaparse@5.4.1: resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==} @@ -4239,6 +4319,9 @@ packages: peek-stream@1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -4433,6 +4516,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -4745,6 +4831,9 @@ packages: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -4808,6 +4897,10 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -5095,6 +5188,10 @@ packages: resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -5216,6 +5313,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + tailwind-merge@2.5.2: resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==} @@ -5248,6 +5349,9 @@ packages: engines: {node: '>=10'} hasBin: true + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -5481,6 +5585,9 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -6479,6 +6586,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.6': {} + '@babel/template@7.25.0': dependencies: '@babel/code-frame': 7.24.7 @@ -7825,6 +7934,10 @@ snapshots: dependencies: '@types/node': 22.6.1 + '@types/jspdf@2.0.0': + dependencies: + jspdf: 4.1.0 + '@types/mdast@3.0.15': dependencies: '@types/unist': 2.0.11 @@ -7858,6 +7971,8 @@ snapshots: dependencies: '@types/node': 22.6.1 + '@types/pako@2.0.4': {} + '@types/passport@1.0.16': dependencies: '@types/express': 5.0.0 @@ -7866,6 +7981,9 @@ snapshots: '@types/qs@6.9.16': {} + '@types/raf@3.4.3': + optional: true + '@types/range-parser@1.2.7': {} '@types/react-dom@18.3.1': @@ -7890,6 +8008,9 @@ snapshots: '@types/node': 22.6.1 '@types/send': 0.17.4 + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)': @@ -8330,6 +8451,9 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: + optional: true + base64-js@1.5.1: {} base64url@3.0.1: {} @@ -8449,6 +8573,18 @@ snapshots: caniuse-lite@1.0.30001663: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.28.6 + '@types/raf': 3.4.3 + core-js: 3.48.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + capital-case@1.0.4: dependencies: no-case: 3.0.4 @@ -8633,6 +8769,9 @@ snapshots: dependencies: toggle-selection: 1.0.6 + core-js@3.48.0: + optional: true + core-util-is@1.0.3: {} cross-spawn@7.0.3: @@ -8641,6 +8780,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + css-what@6.1.0: {} cssesc@3.0.0: {} @@ -8782,6 +8926,11 @@ snapshots: '@babel/runtime': 7.25.6 csstype: 3.1.3 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + optional: true + dot-case@3.0.4: dependencies: no-case: 3.0.4 @@ -9059,7 +9208,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.11.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.11.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -9072,7 +9221,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.11.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.11.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -9094,7 +9243,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.11.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.11.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -9317,6 +9466,12 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + fast-xml-parser@4.4.1: dependencies: strnum: 1.0.5 @@ -9335,6 +9490,8 @@ snapshots: fflate@0.4.8: {} + fflate@0.8.2: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -9588,6 +9745,12 @@ snapshots: dependencies: lru-cache: 7.18.3 + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -9649,6 +9812,8 @@ snapshots: internmap@2.0.3: {} + iobuffer@5.4.0: {} + ipaddr.js@1.9.1: {} is-alphabetical@2.0.1: {} @@ -9874,6 +10039,17 @@ snapshots: ms: 2.1.3 semver: 5.7.2 + jspdf@4.1.0: + dependencies: + '@babel/runtime': 7.28.6 + fast-png: 6.4.0 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.48.0 + dompurify: 3.3.1 + html2canvas: 1.4.1 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -10608,6 +10784,8 @@ snapshots: pako@0.2.9: {} + pako@2.1.0: {} + papaparse@5.4.1: {} param-case@3.0.4: @@ -10693,6 +10871,9 @@ snapshots: duplexify: 3.7.1 through2: 2.0.5 + performance-now@2.1.0: + optional: true + periscopic@3.1.0: dependencies: '@types/estree': 1.0.6 @@ -10877,6 +11058,11 @@ snapshots: queue-microtask@1.2.3: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + range-parser@1.2.1: {} raw-body@2.5.2: @@ -11304,6 +11490,9 @@ snapshots: globalthis: 1.0.4 which-builtin-type: 1.1.4 + regenerator-runtime@0.13.11: + optional: true + regenerator-runtime@0.14.1: {} regexp-to-ast@0.5.0: {} @@ -11382,6 +11571,9 @@ snapshots: reusify@1.0.4: {} + rgbcolor@1.0.1: + optional: true + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -11695,6 +11887,9 @@ snapshots: dependencies: minipass: 7.1.2 + stackblur-canvas@2.7.0: + optional: true + statuses@2.0.1: {} stream-shift@1.0.3: {} @@ -11835,6 +12030,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-pathdata@6.0.3: + optional: true + tailwind-merge@2.5.2: {} tailwindcss@3.4.13: @@ -11903,6 +12101,11 @@ snapshots: source-map-support: 0.5.21 optional: true + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + text-table@0.2.0: {} thenify-all@1.6.0: @@ -12154,6 +12357,11 @@ snapshots: utils-merge@1.0.1: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + uuid@10.0.0: {} uuid@8.3.2: {}