diff --git a/llm-fe/src/App.tsx b/llm-fe/src/App.tsx index 10c69e6..a4ecc67 100644 --- a/llm-fe/src/App.tsx +++ b/llm-fe/src/App.tsx @@ -19,61 +19,64 @@ import DocumentStoragePage from './llm-fe/pages/DocumentStoragePage/DocumentStor import Account2 from './llm-fe/pages/Account2/Account2'; import AnalyticsPage from './llm-fe/pages/Analytics/Analytics'; import PasswordResetConfirmation from './llm-fe/pages/PasswordResetConfirmation/PasswordReset'; +import GlobalThemeWrapper from './llm-fe/components/GlobalThemeWrapper/GlobalThemeWrapper'; const ProtectedRoutes = () => { const { authenticated, needsNewPassword, loading } = useContext(AuthContext); - if(loading){ - return( + if (loading) { + return (
Loading
) } - if(!authenticated) return + if (!authenticated) return //if(!needsNewPassword) return - return + return } class App extends Component { - + render() { return ( -
-
-
- - - - } /> - - - - - - + +
+
+
- - }> - - - - - - - + + } /> + + + + + + + + + }> + + + + + + + + + + + +
+ +
+
+
+ -
- -
- -
-
- - ); } - + } export default App; diff --git a/llm-fe/src/axiosApi.js b/llm-fe/src/axiosApi.js index 1ba59f3..79fbd08 100644 --- a/llm-fe/src/axiosApi.js +++ b/llm-fe/src/axiosApi.js @@ -1,95 +1,97 @@ import axios from "axios"; -const Cookies = require('js-cookie'); +const Cookies = require("js-cookie"); -//const baseURL = 'http://127.0.0.1:8011/api/'; -const baseURL = 'https://chatbackend.aimloperations.com/api/'; +const baseURL = "http://localhost:8011/api/"; +//const baseURL = 'https://chatbackend.aimloperations.com/api/'; //const baseURL = process.env.REACT_APP_BACKEND_REST_API_BASE_URL; export const axiosInstance = axios.create({ - baseURL: baseURL, - timeout: 5000, - headers: { - "Authorization": 'JWT ' + localStorage.getItem('access_token'), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } + baseURL: baseURL, + timeout: 5000, + headers: { + Authorization: "JWT " + localStorage.getItem("access_token"), + "Content-Type": "application/json", + Accept: "application/json", + }, }); export const cleanAxiosInstance = axios.create({ - baseURL: baseURL, - timeout: 5000, - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } + baseURL: baseURL, + timeout: 5000, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, }); export const axiosInstanceCSRF = axios.create({ - baseURL: baseURL, - timeout: 5000, - headers: { - 'X-CSRFToken': Cookies.get('csrftoken'), // Include CSRF token in headers - }, - withCredentials: true, - } -); + baseURL: baseURL, + timeout: 5000, + headers: { + "X-CSRFToken": Cookies.get("csrftoken"), // Include CSRF token in headers + }, + withCredentials: true, +}); -axiosInstance.interceptors.request.use(config => { - config.timeout = 100000; - return config; -}) +axiosInstance.interceptors.request.use((config) => { + config.timeout = 100000; + return config; +}); axiosInstance.interceptors.response.use( - response => response, - error => { - const originalRequest = error.config; + (response) => response, + (error) => { + const originalRequest = error.config; - // Prevent infinite loop - if (error.response.status === 401 && originalRequest.url === baseURL+'/token/refresh/') { - window.location.href = '/signin/'; - //console.log('remove the local storage here') - return Promise.reject(error); - } - - if(error.response.data.code === "token_not_valid" && - error.response.status == 401 && - error.response.statusText == 'Unauthorized') - { - const refresh_token = localStorage.getItem('refresh_token'); - - if (refresh_token){ - const tokenParts = JSON.parse(atob(refresh_token.split('.')[1])); - - const now = Math.ceil(Date.now() / 1000); - //console.log(tokenParts.exp) - - if(tokenParts.exp > now){ - return axiosInstance.post('/token/refresh/', {refresh: refresh_token}).then((response) => { - localStorage.setItem('access_token', response.data.access); - localStorage.setItem('refresh_token', response.data.refresh); - - axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access; - originalRequest.headers['Authorization'] = 'JWT ' + response.data.access; - - return axiosInstance(originalRequest); - }).catch(err => { - console.log(err) - }); - - }else{ - console.log('Refresh token is expired'); - window.location.href = '/signin/'; - - } - }else { - console.log('Refresh token not available'); - window.location.href = '/signin/'; - - } - - - - } - return Promise.reject(error); + // Prevent infinite loop + if ( + error.response.status === 401 && + originalRequest.url === baseURL + "/token/refresh/" + ) { + window.location.href = "/signin/"; + //console.log('remove the local storage here') + return Promise.reject(error); } -); \ No newline at end of file + + if ( + error.response.data.code === "token_not_valid" && + error.response.status == 401 && + error.response.statusText == "Unauthorized" + ) { + const refresh_token = localStorage.getItem("refresh_token"); + + if (refresh_token) { + const tokenParts = JSON.parse(atob(refresh_token.split(".")[1])); + + const now = Math.ceil(Date.now() / 1000); + //console.log(tokenParts.exp) + + if (tokenParts.exp > now) { + return axiosInstance + .post("/token/refresh/", { refresh: refresh_token }) + .then((response) => { + localStorage.setItem("access_token", response.data.access); + localStorage.setItem("refresh_token", response.data.refresh); + + axiosInstance.defaults.headers["Authorization"] = + "JWT " + response.data.access; + originalRequest.headers["Authorization"] = + "JWT " + response.data.access; + + return axiosInstance(originalRequest); + }) + .catch((err) => { + console.log(err); + }); + } else { + console.log("Refresh token is expired"); + window.location.href = "/signin/"; + } + } else { + console.log("Refresh token not available"); + window.location.href = "/signin/"; + } + } + return Promise.reject(error); + }, +); diff --git a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx index 6b19b60..2f14782 100644 --- a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx +++ b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx @@ -1,22 +1,79 @@ import React from "react"; import Markdown from "markdown-to-jsx"; -import { Card, CardContent, CircularProgress } from "@mui/material"; -import styled from "styled-components"; +import styled, { keyframes } from "styled-components"; -const StyleDiv = styled.div` - background-color: #f0f0f0; - padding: 20px; - border-radius: 8px; +const fadeIn = keyframes` + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +`; - h1 { - color: #333; - font-size: 24px; +const MessageContainer = styled.div<{ $isUser: boolean }>` + display: flex; + justify-content: ${(props) => (props.$isUser ? "flex-end" : "flex-start")}; + margin-bottom: 1.5rem; + width: 100%; + animation: ${fadeIn} 0.3s ease-out; +`; + +const Bubble = styled.div<{ $isUser: boolean }>` + max-width: 80%; + padding: 1rem 1.5rem; + border-radius: 1.2rem; + background: ${(props) => + props.$isUser + ? `linear-gradient(135deg, ${props.theme.main} 0%, ${props.theme.focus} 100%)` + : props.theme.darkMode + ? "rgba(255, 255, 255, 0.1)" + : "rgba(0, 0, 0, 0.7)"}; // Dark background for AI in light mode as requested + color: #fff; // Text stays white as background is always dark/colored + backdrop-filter: blur(10px); + border: 1px solid ${(props) => props.theme.darkMode ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)"}; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + font-size: 1rem; + line-height: 1.6; + border-bottom-right-radius: ${(props) => (props.$isUser ? "0.2rem" : "1.2rem")}; + border-bottom-left-radius: ${(props) => (props.$isUser ? "1.2rem" : "0.2rem")}; + + & pre { + background: rgba(0, 0, 0, 0.3); + padding: 1rem; + border-radius: 0.5rem; + overflow-x: auto; } - span { - color: #666; - font-size: 16px; + & code { + font-family: 'Fira Code', monospace; + font-size: 0.9em; } + + & a { + color: #a0c4ff; + text-decoration: underline; + } +`; + +const LoadingDot = styled.div` + width: 8px; + height: 8px; + background: #fff; + border-radius: 50%; + margin: 0 4px; + animation: bounce 1.4s infinite ease-in-out both; + + &:nth-child(1) { animation-delay: -0.32s; } + &:nth-child(2) { animation-delay: -0.16s; } + + @keyframes bounce { + 0%, 80%, 100% { transform: scale(0); } + 40% { transform: scale(1); } + } +`; + +const LoadingContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; `; type ConversationDetailCardProps = { @@ -30,7 +87,7 @@ const MyPlot = ({ format, image }: { format: string; image: string }) => { return ( plot ); @@ -39,7 +96,9 @@ const MyPlot = ({ format, image }: { format: string; image: string }) => { // Custom component for rendering errors const MyError = ({ content }: { content: string }) => { return ( - Error: {content} + + Error: {content} + ); }; @@ -47,85 +106,66 @@ const ConversationDetailCard = ({ message, user_created, }: ConversationDetailCardProps): JSX.Element => { - const text_align = user_created ? "right" : "left"; if (message.length === 0) { return ( - - - - - - ); - } else { - let contentToAdd = message; - console.log(contentToAdd); - try { - const parsedMessage = JSON.parse(message); - if ( - parsedMessage && - typeof parsedMessage === "object" && - parsedMessage.type - ) { - switch (parsedMessage.type) { - case "text": - contentToAdd = parsedMessage.content; - - break; - case "plot": - contentToAdd = ``; - break; - case "error": - contentToAdd = ``; - break; - default: - // contentToAdd is already `message` - - break; - } - } - } catch {} - console.log(contentToAdd); - - return ( - - - - {contentToAdd} - - - + + + + + + + + + ); } + + let contentToAdd = message; + try { + const parsedMessage = JSON.parse(message); + if ( + parsedMessage && + typeof parsedMessage === "object" && + parsedMessage.type + ) { + switch (parsedMessage.type) { + case "text": + contentToAdd = parsedMessage.content; + break; + case "plot": + contentToAdd = ``; + break; + case "error": + contentToAdd = ``; + break; + default: + break; + } + } + } catch { } + + return ( + + + + {contentToAdd} + + + + ); }; export default ConversationDetailCard; diff --git a/llm-fe/src/llm-fe/components/DashboardWrapperLayout/DashboardWrapperLayout.tsx b/llm-fe/src/llm-fe/components/DashboardWrapperLayout/DashboardWrapperLayout.tsx index a1ceac3..b4015c0 100644 --- a/llm-fe/src/llm-fe/components/DashboardWrapperLayout/DashboardWrapperLayout.tsx +++ b/llm-fe/src/llm-fe/components/DashboardWrapperLayout/DashboardWrapperLayout.tsx @@ -8,36 +8,39 @@ import { CacheProvider } from "@emotion/react"; import { CssBaseline, Icon, ThemeProvider } from "@mui/material"; import brandWhite from "../../ui-kit/assets/images/logo-ct.png"; import brandDark from "../../ui-kit/assets/images/logo-ct-dark.png"; -import themeDark from "../../ui-kit/assets/theme-dark"; +import createDarkTheme from "../../ui-kit/assets/theme-dark"; import themeDarkRTL from "../../ui-kit/assets/theme-dark/theme-rtl"; -import theme from "../../ui-kit/assets/theme"; +import createTheme from "../../ui-kit/assets/theme"; import themeRTL from "../../ui-kit/assets/theme/theme-rtl"; +import palettes from "../../ui-kit/assets/theme/base/palettes"; import Sidenav from "../../ui-kit/examples/Sidenav"; import Configurator from "../../ui-kit/examples/Configurator"; type DashboardWrapperLayoutProps = { - children: React.ReactNode; + children: React.ReactNode; } -const DashboardWrapperLayout = ({children}: DashboardWrapperLayoutProps): JSX.Element => { - - const [controller, dispatch] = useMaterialUIController(); - const { - miniSidenav, - direction, - layout, - openConfigurator, - sidenavColor, - transparentSidenav, - whiteSidenav, - darkMode, - } = controller; - const [onMouseEnter, setOnMouseEnter] = useState(false); - const [rtlCache, setRtlCache] = useState(null); - const { pathname } = useLocation(); +const DashboardWrapperLayout = ({ children }: DashboardWrapperLayoutProps): JSX.Element => { - // Cache for the rtl + const [controller, dispatch] = useMaterialUIController(); + const { + miniSidenav, + direction, + layout, + openConfigurator, + sidenavColor, + transparentSidenav, + whiteSidenav, + + darkMode, + themeColor, + } = controller; + const [onMouseEnter, setOnMouseEnter] = useState(false); + const [rtlCache, setRtlCache] = useState(null); + const { pathname } = useLocation(); + + // Cache for the rtl useMemo(() => { const cacheRtl = createCache({ key: "rtl", @@ -74,8 +77,8 @@ const DashboardWrapperLayout = ({children}: DashboardWrapperLayoutProps): JSX.El // Setting page scroll to 0 when changing the route useEffect(() => { document.documentElement.scrollTop = 0; - if(document.scrollingElement){ - document.scrollingElement.scrollTop = 0; + if (document.scrollingElement) { + document.scrollingElement.scrollTop = 0; } }, [pathname]); @@ -126,7 +129,7 @@ const DashboardWrapperLayout = ({children}: DashboardWrapperLayoutProps): JSX.El ) : ( - + {layout === "dashboard" && ( <> @@ -146,7 +149,7 @@ const DashboardWrapperLayout = ({children}: DashboardWrapperLayoutProps): JSX.El {children} ); - + } export default DashboardWrapperLayout; \ No newline at end of file diff --git a/llm-fe/src/llm-fe/components/GlobalThemeWrapper/GlobalThemeWrapper.tsx b/llm-fe/src/llm-fe/components/GlobalThemeWrapper/GlobalThemeWrapper.tsx new file mode 100644 index 0000000..7264ead --- /dev/null +++ b/llm-fe/src/llm-fe/components/GlobalThemeWrapper/GlobalThemeWrapper.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { useMaterialUIController } from '../../ui-kit/context'; +import palettes from '../../ui-kit/assets/theme/base/palettes'; + +type GlobalThemeWrapperProps = { + children: React.ReactNode; +}; + +const GlobalThemeWrapper = ({ children }: GlobalThemeWrapperProps): JSX.Element => { + const [controller] = useMaterialUIController(); + const { themeColor, darkMode } = controller; + + // Default to blue if themeColor is not found + const selectedPalette = palettes[themeColor as keyof typeof palettes] || palettes.blue; + + const theme = { + main: selectedPalette.main, + focus: selectedPalette.focus, + darkMode: darkMode, + // Add other global theme variables here if needed + colors: { + background: darkMode ? '#1a2035' : '#f0f2f5', + text: darkMode ? '#ffffff' : '#344767', + cardBackground: darkMode ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.8)', + cardBorder: darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)', + } + }; + + return ( + + {children} + + ); +}; + +export default GlobalThemeWrapper; diff --git a/llm-fe/src/llm-fe/components/Header2/Header2.tsx b/llm-fe/src/llm-fe/components/Header2/Header2.tsx index cb72029..701544d 100644 --- a/llm-fe/src/llm-fe/components/Header2/Header2.tsx +++ b/llm-fe/src/llm-fe/components/Header2/Header2.tsx @@ -1,104 +1,185 @@ -import { AppBar, Button, Toolbar } from '@mui/material'; import React, { useContext, useState } from 'react'; -import { useMaterialUIController } from '../../ui-kit/context'; -import { boolean } from 'yup'; -import MDBox from '../../ui-kit/components/MDBox'; -import { - navbar, - navbarContainer, - navbarRow, - navbarIconButton, - navbarMobileMenu, - } from "../../ui-kit/examples/Navbars/DashboardNavbar/styles"; -import Breadcrumbs from '../../ui-kit/examples/Breadcrumbs'; -import { AccountContext } from '../../contexts/AccountContext'; -import { AuthContext } from '../../contexts/AuthContext'; +import styled, { useTheme } from 'styled-components'; import { useNavigate } from 'react-router-dom'; +import { AuthContext } from '../../contexts/AuthContext'; +import { AccountContext } from '../../contexts/AccountContext'; import { axiosInstance } from '../../../axiosApi'; +const HeaderContainer = styled.header` + position: absolute; + top: 0; + left: 0; + width: 100%; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + z-index: 100; + background: ${({ theme }) => theme.darkMode ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)'}; + backdrop-filter: blur(5px); + border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder}; +`; +const Logo = styled.div` + font-size: 1.5rem; + font-weight: 700; + color: ${({ theme }) => theme.colors.text}; + cursor: pointer; + background: ${({ theme }) => `linear-gradient(135deg, ${theme.main} 0%, ${theme.focus} 100%)`}; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +`; + +const Nav = styled.nav` + display: flex; + gap: 1.5rem; + + @media (max-width: 768px) { + display: none; + } +`; + +const NavLink = styled.button` + background: transparent; + border: none; + color: ${({ theme }) => theme.colors.text}; + opacity: 0.7; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + + &:hover { + opacity: 1; + background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}; + } +`; + +const SignOutButton = styled(NavLink)` + color: #ff6b6b; + opacity: 1; + + &:hover { + color: #ff4757; + background: rgba(255, 71, 87, 0.1); + } +`; + +const MobileMenuButton = styled.button` + display: none; + background: transparent; + border: none; + cursor: pointer; + z-index: 101; + + @media (max-width: 768px) { + display: block; + } +`; + +const MobileMenuDropdown = styled.div<{ isOpen: boolean }>` + position: absolute; + top: 100%; + left: 0; + width: 100%; + background: ${({ theme }) => theme.colors.cardBackground}; + backdrop-filter: blur(10px); + border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder}; + display: flex; + flex-direction: column; + padding: 1rem; + gap: 1rem; + transform: ${({ isOpen }) => (isOpen ? 'translateY(0)' : 'translateY(-20px)')}; + opacity: ${({ isOpen }) => (isOpen ? '1' : '0')}; + pointer-events: ${({ isOpen }) => (isOpen ? 'auto' : 'none')}; + transition: all 0.3s ease; + z-index: 99; +`; + +const HamburgerIcon = ({ color }: { color: string }) => ( + + + + + +); + +const CloseIcon = ({ color }: { color: string }) => ( + + + + +); type Header2Props = { - absolute?: Boolean; - light?: Boolean; - isMini?: Boolean; + absolute?: Boolean; + light?: Boolean; + isMini?: Boolean; } -const Header2 = ({ absolute=false, light=false, isMini=false }: Header2Props): JSX.Element => { - const {authenticated, setAuthentication} = useContext(AuthContext); - const { account, setAccount } = useContext(AccountContext); - const navigate = useNavigate(); - const handleSignOut = async () => { - try{ - const response = await axiosInstance.post('blacklist/',{ - 'refresh_token': localStorage.getItem("refresh_token") - }) - localStorage.removeItem('access_token') - localStorage.removeItem('refresh_token') - axiosInstance.defaults.headers['Authorization'] = null; - setAuthentication(false) - setAccount(undefined); - navigate('/signin/') - }finally{ - - } - } - - const handleDashboardClick = async () => { - navigate('/') - } +const Header2 = ({ absolute = false, light = false, isMini = false }: Header2Props): JSX.Element => { + const { setAuthentication } = useContext(AuthContext); + const { setAccount } = useContext(AccountContext); + const navigate = useNavigate(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const theme = useTheme(); - const handleDocumentStorageClick = async () => { - navigate('/document_storage/') - } - - const handleAccountClick = async () => { - navigate('/account/') - } - - const handleFeedbackClick = async () => { - navigate('/feedback/') - } + const handleSignOut = async () => { + try { + await axiosInstance.post('blacklist/', { + 'refresh_token': localStorage.getItem("refresh_token") + }) + localStorage.removeItem('access_token') + localStorage.removeItem('refresh_token') + axiosInstance.defaults.headers['Authorization'] = null; + setAuthentication(false) + setAccount(undefined); + navigate('/signin/') + } catch (e) { + console.error(e); + } + } - const handleAnalyticsClick = async () => { - navigate('/analytics/') - } - const [navbarType, setNavbarType] = useState(); - const [controller, dispatch] = useMaterialUIController(); - const { miniSidenav, transparentNavbar, fixedNavbar, openConfigurator, darkMode } = controller; - return ( - navbar(theme, { transparentNavbar, absolute, light, darkMode })} - > - {/* sx={(theme) => navbarContainer(theme)}> */} - - - + const handleNavClick = (path: string) => { + navigate(path); + setIsMenuOpen(false); + }; - - - - - + return ( + + navigate('/')}> +

Chat

- - - - - +
-
- - + {/* Desktop Nav */} + -
-
+ {/* Mobile Menu Button */} + setIsMenuOpen(!isMenuOpen)}> + {isMenuOpen ? : } + - - - ) + {/* Mobile Menu Dropdown */} + + handleNavClick('/')}>Dashboard + handleNavClick('/account/')}>Account + handleNavClick('/document_storage/')}>Documents + handleNavClick('/analytics/')}>Analytics + handleNavClick('/feedback/')}>Feedback + { handleSignOut(); setIsMenuOpen(false); }}>Sign Out + + + ) } export default Header2; \ No newline at end of file diff --git a/llm-fe/src/llm-fe/components/PageWrapperLayout/PageWrapperLayout.tsx b/llm-fe/src/llm-fe/components/PageWrapperLayout/PageWrapperLayout.tsx index ef15993..9679b6b 100644 --- a/llm-fe/src/llm-fe/components/PageWrapperLayout/PageWrapperLayout.tsx +++ b/llm-fe/src/llm-fe/components/PageWrapperLayout/PageWrapperLayout.tsx @@ -6,27 +6,29 @@ import stylisRTLPlugin from "stylis-plugin-rtl"; import MDBox from "../../ui-kit/components/MDBox"; import { CacheProvider } from "@emotion/react"; import { CssBaseline, Icon, ThemeProvider } from "@mui/material"; -import themeDark from "../../ui-kit/assets/theme-dark"; -import theme from "../../ui-kit/assets/theme"; +import createTheme from "../../ui-kit/assets/theme"; +import createDarkTheme from "../../ui-kit/assets/theme-dark"; import themeRtl from "../../ui-kit/assets/theme/theme-rtl"; import themeDarkRTL from "../../ui-kit/assets/theme-dark/theme-rtl"; +import palettes from "../../ui-kit/assets/theme/base/palettes"; type PageWrapperLayoutProps = { - children: React.ReactNode; + children: React.ReactNode; } -const PageWrapperLayout = ({children}: PageWrapperLayoutProps): JSX.Element => { - - const [controller, dispatch] = useMaterialUIController(); - const { - direction, - openConfigurator, - darkMode, - } = controller; - const [rtlCache, setRtlCache] = useState(null); - const { pathname } = useLocation(); +const PageWrapperLayout = ({ children }: PageWrapperLayoutProps): JSX.Element => { - // Cache for the rtl + const [controller, dispatch] = useMaterialUIController(); + const { + direction, + openConfigurator, + darkMode, + themeColor, + } = controller; + const [rtlCache, setRtlCache] = useState(null); + const { pathname } = useLocation(); + + // Cache for the rtl useMemo(() => { const cacheRtl = createCache({ key: "rtl", @@ -47,8 +49,8 @@ const PageWrapperLayout = ({children}: PageWrapperLayoutProps): JSX.Element => { // Setting page scroll to 0 when changing the route useEffect(() => { document.documentElement.scrollTop = 0; - if(document.scrollingElement){ - document.scrollingElement.scrollTop = 0; + if (document.scrollingElement) { + document.scrollingElement.scrollTop = 0; } }, [pathname]); @@ -81,19 +83,19 @@ const PageWrapperLayout = ({children}: PageWrapperLayoutProps): JSX.Element => { {children} - - + + ) : ( - + - {children} - + {children} + ); - + } export default PageWrapperLayout; \ No newline at end of file diff --git a/llm-fe/src/llm-fe/components/ParticleBackground/ParticleBackground.tsx b/llm-fe/src/llm-fe/components/ParticleBackground/ParticleBackground.tsx new file mode 100644 index 0000000..1085359 --- /dev/null +++ b/llm-fe/src/llm-fe/components/ParticleBackground/ParticleBackground.tsx @@ -0,0 +1,139 @@ +import React, { useEffect, useRef } from 'react'; +import styled, { useTheme } from 'styled-components'; + +const Canvas = styled.canvas` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background: ${({ theme }) => theme.darkMode ? 'linear-gradient(to bottom, #000000, #1a1a2e)' : 'linear-gradient(to bottom, #f0f2f5, #e0e5ec)'}; + transition: background 0.5s ease; +`; + +const ParticleBackground = () => { + const canvasRef = useRef(null); + const theme = useTheme(); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + let particles: Particle[] = []; + let animationFrameId: number; + + const resizeCanvas = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + + class Particle { + x: number; + y: number; + vx: number; + vy: number; + size: number; + color: string; + + constructor() { + this.x = Math.random() * canvas!.width; + this.y = Math.random() * canvas!.height; + this.vx = (Math.random() - 0.5) * 0.5; + this.vy = (Math.random() - 0.5) * 0.5; + this.size = Math.random() * 2 + 1; + // Use theme main color or fallback + const baseColor = theme?.main || '#6495ed'; + // Convert hex to rgb for opacity if needed, or just use it directly if it's already compatible + // For simplicity, let's assume theme.main is a hex and we want to add opacity. + // But here we are using a fixed color for now, let's try to use the theme color. + this.color = theme?.darkMode + ? `rgba(100, 149, 237, ${Math.random() * 0.5 + 0.2})` + : `rgba(100, 149, 237, ${Math.random() * 0.5 + 0.2})`; // Keep blueish for now, maybe change later + + // Better approach: use the theme main color + this.color = theme?.main + ? `${theme.main}${Math.floor((Math.random() * 0.5 + 0.2) * 255).toString(16).padStart(2, '0')}` + : `rgba(100, 149, 237, ${Math.random() * 0.5 + 0.2})`; + } + + update() { + this.x += this.vx; + this.y += this.vy; + + if (this.x < 0 || this.x > canvas!.width) this.vx *= -1; + if (this.y < 0 || this.y > canvas!.height) this.vy *= -1; + } + + draw() { + if (!ctx) return; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + } + } + + const init = () => { + particles = []; + const numberOfParticles = Math.floor((canvas.width * canvas.height) / 15000); + for (let i = 0; i < numberOfParticles; i++) { + particles.push(new Particle()); + } + }; + + const animate = () => { + if (!ctx) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + particles.forEach((particle) => { + particle.update(); + particle.draw(); + }); + + // Draw connections + for (let i = 0; i < particles.length; i++) { + for (let j = i + 1; j < particles.length; j++) { + const dx = particles[i].x - particles[j].x; + const dy = particles[i].y - particles[j].y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 100) { + ctx.beginPath(); + // Use theme main color for connections too + const opacity = 1 - distance / 100; + const hexOpacity = Math.floor(opacity * 255).toString(16).padStart(2, '0'); + ctx.strokeStyle = theme?.main + ? `${theme.main}${hexOpacity}` + : `rgba(100, 149, 237, ${opacity})`; + + ctx.lineWidth = 0.5; + ctx.moveTo(particles[i].x, particles[i].y); + ctx.lineTo(particles[j].x, particles[j].y); + ctx.stroke(); + } + } + } + + animationFrameId = requestAnimationFrame(animate); + }; + + init(); + animate(); + + return () => { + window.removeEventListener('resize', resizeCanvas); + cancelAnimationFrame(animationFrameId); + }; + }, [theme]); // Re-run when theme changes + + return ; +}; + +export default ParticleBackground; diff --git a/llm-fe/src/llm-fe/components/SetPassword/SetPassword.tsx b/llm-fe/src/llm-fe/components/SetPassword/SetPassword.tsx index 24889ea..03395fb 100644 --- a/llm-fe/src/llm-fe/components/SetPassword/SetPassword.tsx +++ b/llm-fe/src/llm-fe/components/SetPassword/SetPassword.tsx @@ -5,154 +5,169 @@ import { Alert, Button, Stack, Typography } from '@mui/material'; import { axiosInstance } from '../../../axiosApi'; import { useNavigate, useSearchParams } from 'react-router-dom'; import CustomToastMessage from '../../components/CustomToastMessage/CustomeToastMessage'; -import background from '../../../bg.jpeg' + import * as Yup from 'yup'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import ErrorIcon from '@mui/icons-material/Error'; +import PageWrapperLayout from '../PageWrapperLayout/PageWrapperLayout'; +import MDBox from '../../ui-kit/components/MDBox'; +import { Card, CardContent } from '@mui/material'; +import MDTypography from '../../ui-kit/components/MDTypography'; +import MDButton from '../../ui-kit/components/MDButton'; +import ParticleBackground from '../../components/ParticleBackground/ParticleBackground'; + export type PasswordResetValues = { - password1: string; - password2: string; + password1: string; + password2: string; }; -const initialValues = {password1: '', password2: ''} -const validationSchema = Yup.object().shape({ +const initialValues = { password1: '', password2: '' } +const validationSchema = Yup.object().shape({ password1: Yup.string().min(6, "Passwords have to be at least 6 digits").required(), password2: Yup.string().min(6, "Passwords have to be at least 6 digits").required().oneOf([Yup.ref('password1')], "Passwords must match"), }) const contains_number = (item: string): boolean => { - const numbers = ['1','2','3','4','5','6','7','8','9','0']; + const numbers = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; const hasNumber = numbers.some((character) => item.includes(character)); - if(hasNumber){ + if (hasNumber) { return true; } return false; } const contains_special_character = (item: string): boolean => { - const specialCharacters = ['!','@','#','$',',%','^','&','*','(',')','-','_','=','+','/','*','\\','|','`','~','<','>','.','?']; + const specialCharacters = ['!', '@', '#', '$', ',%', '^', '&', '*', '(', ')', '-', '_', '=', '+', '/', '*', '\\', '|', '`', '~', '<', '>', '.', '?']; const hasSpecialChacater = specialCharacters.some((character) => item.includes(character)); - if(hasSpecialChacater){ + if (hasSpecialChacater) { return true; } return false; } -const SetPassword = ({}): JSX.Element => { - const navigate = useNavigate(); +const SetPassword = ({ }): JSX.Element => { + const navigate = useNavigate(); // see if the user is allowed to come here first const [queryParameters] = useSearchParams() console.log(queryParameters.get("slug")) const slug = queryParameters.get("slug"); - - try{ + + try { // make sure it comes back as 200 for a good request. Else go to the homepage axiosInstance.get(`user/set_password/${slug}`) - }catch{ + } catch { navigate('/') } - const handleSetPassword = ({password1, password2}: PasswordResetValues): void => { - try{ - // verify - if(password1 === password2){ - axiosInstance.post(`user/set_password/${slug}/`, { - 'password': password1, - }); - - navigate('/') - } - }catch(error){ - console.log('catching the error'); - - + const handleSetPassword = ({ password1, password2 }: PasswordResetValues): void => { + try { + // verify + if (password1 === password2) { + axiosInstance.post(`user/set_password/${slug}/`, { + 'password': password1, + }); + + navigate('/') + } + } catch (error) { + console.log('catching the error'); + + + } } -} - - return ( -
-
-
-
-
-
-
-

Set your password

-
-
+ + return ( + + + + + + + + + Set your password + +
+ initialValues={initialValues} + onSubmit={handleSetPassword} + validateOnMount + validationSchema={validationSchema}> {(formik) => (
- formik.setFieldValue('password1', e.target.value)} /> + formik.setFieldValue('password1', e.target.value)} />
- formik.setFieldValue('password2', e.target.value)} /> + formik.setFieldValue('password2', e.target.value)} />
- - {formik.values.password1 === formik.values.password2 ? : } - - Passwords Match + + {formik.values.password1 === formik.values.password2 ? : } + + Passwords Match
- - {formik.values.password1.length > 5 ? : } - At least 6 characters + + {formik.values.password1.length > 5 ? : } + At least 6 characters
- - {contains_special_character(formik.values.password1) ? : } - At least one special character + + {contains_special_character(formik.values.password1) ? : } + At least one special character
- - - {contains_number(formik.values.password1) ? : } - At least one number + + + {contains_number(formik.values.password1) ? : } + At least one number
- - + + Set password + +
@@ -162,12 +177,11 @@ const SetPassword = ({}): JSX.Element => {
-
-
-
-
-
- ); + + + + + ); }; export default SetPassword; \ No newline at end of file diff --git a/llm-fe/src/llm-fe/components/ThemeCard/ThemeCard.tsx b/llm-fe/src/llm-fe/components/ThemeCard/ThemeCard.tsx index c368ab7..23274b4 100644 --- a/llm-fe/src/llm-fe/components/ThemeCard/ThemeCard.tsx +++ b/llm-fe/src/llm-fe/components/ThemeCard/ThemeCard.tsx @@ -1,10 +1,12 @@ -import { Card, CardContent, Divider, Switch } from "@mui/material"; +import { Card, CardContent, Divider, Switch, IconButton } from "@mui/material"; import MDTypography from "../../ui-kit/components/MDTypography"; import MDBox from "../../ui-kit/components/MDBox"; import { useMaterialUIController, setDarkMode, + setThemeColor, } from "../../ui-kit/context"; +import palettes from "../../ui-kit/assets/theme/base/palettes"; import { useContext, useEffect, useState } from "react"; import { AxiosResponse } from "axios"; import { axiosInstance } from "../../../axiosApi"; @@ -15,73 +17,119 @@ type PreferencesValues = { order: boolean; } -const ThemeCard = ({}): JSX.Element => { - const [controller, dispatch] = useMaterialUIController(); - const { - darkMode, - } = controller; +const ThemeCard = ({ }): JSX.Element => { + const [controller, dispatch] = useMaterialUIController(); + const { + darkMode, + themeColor, + } = controller; + const themeColors = Object.keys(palettes); - const [order, setOrder] = useState(true); - const {updatePreferences} = useContext(PreferenceContext); + const [order, setOrder] = useState(true); + const { updatePreferences } = useContext(PreferenceContext); - const handleConversationOrder = async ({order}: PreferencesValues): Promise => { - try{ - const {data, }: AxiosResponse = await axiosInstance.post('/conversation_preferences', { - order: order - }) - updatePreferences(); - }catch{ + const handleConversationOrder = async ({ order }: PreferencesValues): Promise => { + try { + const { data, }: AxiosResponse = await axiosInstance.post('/conversation_preferences', { + order: order + }) + updatePreferences(); + } catch { + + } - } - } - async function getConversationOrder(){ - try{ - const {data, }: AxiosResponse = await axiosInstance.get(`/conversation_preferences`); - setOrder(data.order); - - - }catch(error){ - console.log(error) - } - } - - useEffect(()=>{ - getConversationOrder(); - }, []) - - const handleDarkMode = () => setDarkMode(dispatch, !darkMode); - const isThemeReady=false; - return( - - - - Account Preferences + async function getConversationOrder() { + try { + const { data, }: AxiosResponse = await axiosInstance.get(`/conversation_preferences`); + setOrder(data.order); - - - - - {isThemeReady ? ( - - Light / Dark - - - ) : ( - <> - )} - - Converastion Order - { - setOrder(!order); - handleConversationOrder({order: !order}) - }} /> - - - - ) + + } catch (error) { + console.log(error) + } + } + + useEffect(() => { + getConversationOrder(); + }, []) + + const handleDarkMode = () => setDarkMode(dispatch, !darkMode); + const isThemeReady = true; + return ( + + + + Account Preferences + + + + + + + Light / Dark + + + + Theme Color + + {themeColors.map((color) => ( + { + const { + borders: { borderWidth }, + palette: { white, dark, background }, + transitions, + } = theme; + + return { + width: "24px", + height: "24px", + padding: 0, + border: `${borderWidth[1]} solid ${darkMode ? background.sidenav : white.main}`, + borderColor: () => { + let borderColorValue = themeColor === color && dark.main; + + if (darkMode && themeColor === color) { + borderColorValue = white.main; + } + + return borderColorValue; + }, + transition: transitions.create("border-color", { + easing: transitions.easing.sharp, + duration: transitions.duration.shorter, + }), + backgroundColor: palettes[color as keyof typeof palettes].main, + + "&:not(:last-child)": { + mr: 1, + }, + + "&:hover, &:focus, &:active": { + borderColor: darkMode ? white.main : dark.main, + }, + }; + }} + onClick={() => setThemeColor(dispatch, color)} + /> + ))} + + + + + Converastion Order + { + setOrder(!order); + handleConversationOrder({ order: !order }) + }} /> + + + + ) } export default ThemeCard; diff --git a/llm-fe/src/llm-fe/components/ThemeSettingsCard/ThemeSettingsCard.tsx b/llm-fe/src/llm-fe/components/ThemeSettingsCard/ThemeSettingsCard.tsx new file mode 100644 index 0000000..c147f5f --- /dev/null +++ b/llm-fe/src/llm-fe/components/ThemeSettingsCard/ThemeSettingsCard.tsx @@ -0,0 +1,209 @@ +import React, { useContext, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { + useMaterialUIController, + setDarkMode, + setThemeColor, +} from "../../ui-kit/context"; +import palettes from "../../ui-kit/assets/theme/base/palettes"; +import { AxiosResponse } from "axios"; +import { axiosInstance } from "../../../axiosApi"; +import { PreferencesType } from "../../data"; +import { PreferenceContext } from "../../contexts/PreferencesContext"; + +// Styled Components (Reusing/Adapting from Account2/Design System) +const GlassCard = styled.div` + background: ${({ theme }) => theme.colors.cardBackground}; + backdrop-filter: blur(10px); + border: 1px solid ${({ theme }) => theme.colors.cardBorder}; + border-radius: 1rem; + padding: 2rem; + width: 100%; + max-width: 1000px; + margin-bottom: 2rem; + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); +`; + +const CardTitle = styled.h2` + font-size: 1.8rem; + margin-bottom: 1.5rem; + color: ${({ theme }) => theme.colors.text}; + border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder}; + padding-bottom: 1rem; +`; + +const SettingRow = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 0; + border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder}; + + &:last-child { + border-bottom: none; + } +`; + +const SettingLabel = styled.span` + font-size: 1.1rem; + color: ${({ theme }) => theme.colors.text}; + font-weight: 500; +`; + +const ToggleSwitch = styled.label` + position: relative; + display: inline-block; + width: 50px; + height: 24px; +`; + +const Slider = styled.span` + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'}; + transition: .4s; + border-radius: 24px; + + &:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; + } +`; + +const Checkbox = styled.input` + opacity: 0; + width: 0; + height: 0; + + &:checked + ${Slider} { + background-color: ${({ theme }) => theme.main}; + } + + &:checked + ${Slider}:before { + transform: translateX(26px); + } +`; + +const ColorPickerContainer = styled.div` + display: flex; + gap: 0.5rem; +`; + +const ColorButton = styled.button<{ color: string; isSelected: boolean }>` + width: 24px; + height: 24px; + border-radius: 50%; + background-color: ${(props) => props.color}; + border: 2px solid ${(props) => (props.isSelected ? props.theme.colors.text : 'transparent')}; + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + + &:hover { + transform: scale(1.1); + } +`; + +type PreferencesValues = { + order: boolean; +} + +const ThemeSettingsCard = (): JSX.Element => { + const [controller, dispatch] = useMaterialUIController(); + const { + darkMode, + themeColor, + } = controller; + const themeColors = Object.keys(palettes); + + const [order, setOrder] = useState(true); + const { updatePreferences } = useContext(PreferenceContext); + + const handleConversationOrder = async ({ order }: PreferencesValues): Promise => { + try { + await axiosInstance.post('/conversation_preferences', { + order: order + }) + updatePreferences(); + } catch { + // Error handling + } + } + + async function getConversationOrder() { + try { + const { data, }: AxiosResponse = await axiosInstance.get(`/conversation_preferences`); + setOrder(data.order); + } catch (error) { + console.log(error) + } + } + + useEffect(() => { + getConversationOrder(); + }, []) + + const handleDarkMode = () => setDarkMode(dispatch, !darkMode); + + return ( + + Account Preferences + + + Dark Mode + + + + + + + + Theme Color + + {themeColors.map((color) => ( + setThemeColor(dispatch, color)} + title={color} + /> + ))} + + + + + Conversation Order (Newest First) + + { + setOrder(!order); + handleConversationOrder({ order: !order }) + }} + /> + + + + + + ); +}; + +export default ThemeSettingsCard; diff --git a/llm-fe/src/llm-fe/pages/Account2/Account2.tsx b/llm-fe/src/llm-fe/pages/Account2/Account2.tsx index 6b7d58b..f99f06e 100644 --- a/llm-fe/src/llm-fe/pages/Account2/Account2.tsx +++ b/llm-fe/src/llm-fe/pages/Account2/Account2.tsx @@ -1,291 +1,445 @@ import { useContext, useEffect, useState } from "react"; -import PageWrapperLayout from "../../components/PageWrapperLayout/PageWrapperLayout"; import { AccountContext } from "../../contexts/AccountContext"; -import MDTypography from "../../ui-kit/components/MDTypography"; import { Account, AccountType } from "../../data"; import * as Yup from 'yup'; -import { Card, CardContent, Divider, IconButton, InputAdornment, Paper, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from "@mui/material"; -import { DeleteForever, Send } from "@mui/icons-material"; import { axiosInstance } from "../../../axiosApi"; import { AxiosResponse } from "axios"; import { ErrorMessage, Field, Form, Formik } from "formik"; -import MDButton from "../../ui-kit/components/MDButton"; -import MDBox from "../../ui-kit/components/MDBox"; import Header2 from "../../components/Header2/Header2"; -import Footer from "../../components/Footer/Footer"; -import ThemeCard from "../../components/ThemeCard/ThemeCard"; +import ParticleBackground from "../../components/ParticleBackground/ParticleBackground"; +import styled, { ThemeProvider } from "styled-components"; +import ThemeSettingsCard from "../../components/ThemeSettingsCard/ThemeSettingsCard"; +import { useMaterialUIController } from "../../ui-kit/context"; +import palettes from "../../ui-kit/assets/theme/base/palettes"; + +// Styled Components +const PageContainer = styled.div` + position: relative; + width: 100vw; + height: 100vh; + overflow: hidden; + display: flex; + flex-direction: column; + color: ${({ theme }) => theme.colors.text}; + font-family: 'Inter', sans-serif; + /* background-color: ${({ theme }) => theme.colors.background}; Removed to show particles */ +`; + +const ContentWrapper = styled.div` + flex: 1; + overflow-y: auto; + padding: 6rem 2rem 2rem 2rem; + display: flex; + flex-direction: column; + align-items: center; + z-index: 5; + + &::-webkit-scrollbar { + width: 8px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + } +`; + +const GlassCard = styled.div` + background: ${({ theme }) => theme.colors.cardBackground}; + backdrop-filter: blur(10px); + border: 1px solid ${({ theme }) => theme.colors.cardBorder}; + border-radius: 1rem; + padding: 2rem; + width: 100%; + max-width: 1000px; + margin-bottom: 2rem; + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); +`; + +const CardTitle = styled.h2` + font-size: 1.8rem; + margin-bottom: 1.5rem; + color: ${({ theme }) => theme.colors.text}; + border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder}; + padding-bottom: 1rem; +`; + +const StyledTable = styled.table` + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +`; + +const Th = styled.th` + text-align: left; + padding: 1rem; + border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder}; + color: ${({ theme }) => theme.colors.text}; + opacity: 0.7; + font-weight: 600; +`; + +const Td = styled.td` + padding: 1rem; + border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder}; + color: ${({ theme }) => theme.colors.text}; +`; + +const StyledInput = styled.input` + width: 100%; + background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}; + border: 1px solid ${({ theme }) => theme.colors.cardBorder}; + border-radius: 0.5rem; + padding: 0.8rem; + color: ${({ theme }) => theme.colors.text}; + font-size: 1rem; + outline: none; + transition: all 0.3s ease; + + &:focus { + background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}; + border-color: ${({ theme }) => theme.main}; + box-shadow: 0 0 10px ${({ theme }) => theme.main}33; // 33 is approx 20% opacity + } +`; + +const StyledButton = styled.button` + background: ${({ theme }) => theme.main}; + border: none; + border-radius: 0.5rem; + color: #fff; + padding: 0.8rem 1.5rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; + margin-top: 1rem; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px ${({ theme }) => theme.main}66; // 66 is approx 40% opacity + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + } +`; + +const ToggleSwitch = styled.label` + position: relative; + display: inline-block; + width: 50px; + height: 24px; +`; + +const Slider = styled.span` + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'}; + transition: .4s; + border-radius: 24px; + + &:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; + } +`; + +const Checkbox = styled.input` + opacity: 0; + width: 0; + height: 0; + + &:checked + ${Slider} { + background-color: ${({ theme }) => theme.main}; + } + + &:checked + ${Slider}:before { + transform: translateX(26px); + } + + &:disabled + ${Slider} { + opacity: 0.5; + cursor: not-allowed; + } +`; + +const DeleteButton = styled.button` + background: transparent; + border: none; + color: #ff6b6b; + cursor: pointer; + font-size: 1.2rem; + transition: color 0.2s ease; + + &:hover { + color: #ff4757; + } +`; type AccountUpdateValues = { - email: string, + email: string, } type AccountInformationProps = { - account: Account | undefined + account: Account | undefined } type InviteValues = { - email: string + email: string } const validationSchema = Yup.object().shape({ - email: Yup.string().email().required("This is requried") - } + email: Yup.string().email().required("This is required") +} ) type AddUserCardProps = { - getCompanyUsers: ()=> void; + getCompanyUsers: () => void; } type CompanyAccountLineProps = { - user: Account - handleUserUpdate: (email: string, field: string, value: string) => void + user: Account + handleUserUpdate: (email: string, field: string, value: string) => void } -const CompanyAccountLine = ({user, handleUserUpdate}: CompanyAccountLineProps): JSX.Element => { - const {account} = useContext(AccountContext); - - return( - - {user.email} - {user.first_name} - {user.last_name} - - handleUserUpdate(user.email, 'company_manager', event.target.value)} disabled={account?.email === user.email} /> - - - handleUserUpdate(user.email, 'is_active', event.target.value)} /> - - - handleUserUpdate(user.email, 'has_password', event.target.value)} disabled={!user.has_password} /> - - - handleUserUpdate(user.email, 'delete', '')}> - - - - - ) - } +const CompanyAccountLine = ({ user, handleUserUpdate }: CompanyAccountLineProps): JSX.Element => { + const { account } = useContext(AccountContext); - const AddUserCard = ({getCompanyUsers}: AddUserCardProps): JSX.Element => { - const initialValues = {'email': '',} - const handleInvite = async ({email}: InviteValues, {resetForm}: any): Promise => { - try{ - await axiosInstance.post('/user/invite/', { - 'email': email - }) - getCompanyUsers() - - } catch{ - // put a message here - } - resetForm(); - - } - return( - - - - Invite A User - - - - - - - - {(formik) => - -
-
- } - size={'small'} - role={undefined} - tabIndex={-1} - margin={"dense"} - variant={"outlined"} - InputProps={{ - endAdornment: ( - - } - disabled={!formik.isValid || formik.isSubmitting}> - <> - - - - ) - }} - > - - -
-
- - - } -
- - -
-
- ) - } - - const CompanyManagerCard = ({}): JSX.Element => { - const [users, setUsers] = useState([]); - const {account}= useContext(AccountContext); - const [showModal, setShowModal] = useState(false); - - const handleUserUpdate = async(email: string, field: string, value: string): Promise => { - //console.log(email, field, value) - if(field === 'delete'){ - await axiosInstance.delete(`/company_users`, { - data: {'email':email} - - }) - }else { - await axiosInstance.post(`/company_users`, { - 'email':email, - 'field': field, - 'value': value - }); - } - - - - // get all of th edata again - try{ - const {data, }: AxiosResponse = await axiosInstance.get(`/company_users`); - setUsers(data.map((item) => new Account({ - - email: item.email, - first_name: item.first_name, - last_name: item.last_name, - is_company_manager: item.is_company_manager, - has_password: item.has_usable_password, - is_active: item.is_active, - company: undefined - }))) - - - }catch(error){ - console.log(error) - } - - } - - async function getCompanyUsers(){ - try{ - const {data, }: AxiosResponse = await axiosInstance.get(`/company_users`); - setUsers(data.map((item) => new Account({ - - email: item.email, - first_name: item.first_name, - last_name: item.last_name, - is_company_manager: item.is_company_manager, - has_password: item.has_usable_password, - is_active: item.is_active, - has_signed_tos: item.has_signed_tos, - company: undefined - }))) - - - }catch(error){ - console.log(error) - } - } - - useEffect(()=>{ - getCompanyUsers(); - }, []) - - return( - <> - - - - Company Accounts - - - - - - - - - - - Email - First Name - Last Name - Is Manager - Is Active - Has Password - Delete - - - - {users.map((user) => )} - - -
-
- -
-
- - - - ) - } - -const AccountPage =({}): JSX.Element => { - const { account } = useContext(AccountContext) - if(account?.is_company_manager){ - - return( - <> - - - - - ) - - }else{ - return( - <> - - Account and prompt information will be available soon - - ) - } - -} - -const Account2 = ({}): JSX.Element => { return ( - + + {user.email} + {user.first_name} + {user.last_name} + + + handleUserUpdate(user.email, 'company_manager', String(event.target.checked))} + disabled={account?.email === user.email} + /> + + + + + + handleUserUpdate(user.email, 'is_active', String(event.target.checked))} + /> + + + + + + handleUserUpdate(user.email, 'has_password', String(event.target.checked))} + disabled={!user.has_password} + /> + + + + + handleUserUpdate(user.email, 'delete', '')}> + 🗑️ + + + + ) +} + +const AddUserCard = ({ getCompanyUsers }: AddUserCardProps): JSX.Element => { + const initialValues = { 'email': '', } + const handleInvite = async ({ email }: InviteValues, { resetForm }: any): Promise => { + try { + await axiosInstance.post('/user/invite/', { + 'email': email + }) + getCompanyUsers() + + } catch { + // put a message here + } + resetForm(); + + } + return ( + + Invite A User + + {(formik) => +
+
+
+ + + {msg =>
{msg}
} +
+
+ + Invite + +
+
+ } +
+
+ ) +} + +const CompanyManagerCard = ({ }): JSX.Element => { + const [users, setUsers] = useState([]); + const { account } = useContext(AccountContext); + const [showModal, setShowModal] = useState(false); + + const handleUserUpdate = async (email: string, field: string, value: string): Promise => { + //console.log(email, field, value) + if (field === 'delete') { + await axiosInstance.delete(`/company_users`, { + data: { 'email': email } + + }) + } else { + await axiosInstance.post(`/company_users`, { + 'email': email, + 'field': field, + 'value': value + }); + } + + + + // get all of th edata again + try { + const { data, }: AxiosResponse = await axiosInstance.get(`/company_users`); + setUsers(data.map((item) => new Account({ + + email: item.email, + first_name: item.first_name, + last_name: item.last_name, + is_company_manager: item.is_company_manager, + has_password: item.has_usable_password, + is_active: item.is_active, + company: undefined + }))) + + + } catch (error) { + console.log(error) + } + + } + + async function getCompanyUsers() { + try { + const { data, }: AxiosResponse = await axiosInstance.get(`/company_users`); + setUsers(data.map((item) => new Account({ + + email: item.email, + first_name: item.first_name, + last_name: item.last_name, + is_company_manager: item.is_company_manager, + has_password: item.has_usable_password, + is_active: item.is_active, + has_signed_tos: item.has_signed_tos, + company: undefined + }))) + + + } catch (error) { + console.log(error) + } + } + + useEffect(() => { + getCompanyUsers(); + }, []) + + return ( + <> + + Company Accounts +
+ + + + Email + First Name + Last Name + Is Manager + Is Active + Has Password + Delete + + + + {users.map((user) => )} + + +
+
+ + + + ) +} + +const AccountPage = ({ }): JSX.Element => { + const { account } = useContext(AccountContext) + + return ( + <> + + {account?.is_company_manager ? ( + + ) : ( + + Account Information +

Account and prompt information will be available soon

+
+ )} + + ) +} + +const Account2 = ({ }): JSX.Element => { + return ( + + - - - - + -