diff --git a/src/App.tsx b/src/App.tsx index da6be30..f1574c4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { ThemeProvider } from './providers/ThemeProvider'; import { UserProvider } from './providers/UserProvider'; import Settings from './components/Settings/Settings'; import { NotificationProvider } from './providers/NotificationProvider'; +import CreateOrg from './components/CreateOrg/CreateOrg'; // The main component of the application function App() { @@ -21,6 +22,10 @@ function App() { + { + expect(true).toBe(true); +}); diff --git a/src/components/ChoiceNotification/ChoiceNotification.tsx b/src/components/ChoiceNotification/ChoiceNotification.tsx new file mode 100644 index 0000000..99372e0 --- /dev/null +++ b/src/components/ChoiceNotification/ChoiceNotification.tsx @@ -0,0 +1,40 @@ +import './ChoiceNotification.scss'; + +interface Props { + heading: string; + info: string; + onAccept: () => void; + onReject: () => void; + acceptText: string; + rejectText: string; +} + +/** + * A notification that is displayed to the user + * @param heading The heading of the notification + * @param info More information about the notification + * @param onClose The function to call when the notification is closed + */ +function ChoiceNotification({ + heading, + info, + onAccept, + onReject, + acceptText = 'Yes', + rejectText = 'No', +}: Props) { + return ( +
+
+
+

{heading}

+

{info}

+
+ + +
+
+
+ ); +} +export default ChoiceNotification; diff --git a/src/components/CreateOrg/CreateOrg.scss b/src/components/CreateOrg/CreateOrg.scss new file mode 100644 index 0000000..8f741d9 --- /dev/null +++ b/src/components/CreateOrg/CreateOrg.scss @@ -0,0 +1,43 @@ +.CreateOrg { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + padding-top: 100px; + h1 { + text-align: center; + } + .errorMsg { + color: red; + font-size: 15px; + font-weight: bold; + text-align: center; + border: none; + box-shadow: none; + min-height: 20px; + } + div { + display: flex; + flex-direction: column; + justify-content: center; + * { + margin: 10px; + } + input { + width: 300px; + height: 30px; + border-radius: 5px; + text-align: center; + font-size: 20px; + background-color: var(--btn-color); + border: 2px solid var(--btn-color); + color: var(--text-color); + &:focus { + outline: none; + border: 2px solid #083d60; + } + } + } +} diff --git a/src/components/CreateOrg/CreateOrg.test.tsx b/src/components/CreateOrg/CreateOrg.test.tsx new file mode 100644 index 0000000..c151265 --- /dev/null +++ b/src/components/CreateOrg/CreateOrg.test.tsx @@ -0,0 +1,3 @@ +test('CreateOrgTest', () => { + expect(true).toBe(true); +}); diff --git a/src/components/CreateOrg/CreateOrg.tsx b/src/components/CreateOrg/CreateOrg.tsx new file mode 100644 index 0000000..bcedcd6 --- /dev/null +++ b/src/components/CreateOrg/CreateOrg.tsx @@ -0,0 +1,85 @@ +import { Link, useNavigate } from 'react-router-dom'; +import './CreateOrg.scss'; +import { useGetUsername, useLoggedIn } from '../../providers/UserProvider'; +import { useEffect, useState } from 'react'; +import APIService from '../../utils/ApiService'; +import { useShowNotification } from '../../providers/NotificationProvider'; + +/** + * A page to create an organization + */ +function CreateOrg() { + const [errorMessage, setErrorMessage] = useState(' '); + const [name, setName] = useState(''); + + const navigate = useNavigate(); + const showNotification = useShowNotification(); + + const getUserName = useGetUsername(); + const isLoggedIn = useLoggedIn(); + + const [username] = useState(getUserName()); + + const onCreateOrg = () => { + if (name === '') { + setErrorMessage('Organization name cannot be empty'); + return; + } + if (name.length > 20) { + setErrorMessage( + 'Organization name cannot be longer than 20 characters' + ); + return; + } + if (name.length < 3) { + setErrorMessage( + 'Organization name cannot be shorter than 3 characters' + ); + return; + } + APIService.createOrg(username, name).then((suc) => { + if (!suc) { + setErrorMessage('Organization already exists'); + return; + } + showNotification( + 'Organization created!', + 'Organization ' + name + ' created successfully.' + ); + navigate('/profile'); + }); + }; + + useEffect(() => { + if (!isLoggedIn) { + navigate('/'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoggedIn, username]); + + return ( +
+

+ wannadb
+ CREATE ORGANIZATION +

+
+

{errorMessage}

+ setName(e.target.value)} + /> + + + + Back + +
+
+ ); +} +export default CreateOrg; diff --git a/src/components/Profile/Profile.scss b/src/components/Profile/Profile.scss index 9331954..b961dfc 100644 --- a/src/components/Profile/Profile.scss +++ b/src/components/Profile/Profile.scss @@ -9,4 +9,10 @@ font-weight: bold; margin-bottom: 20px; } + .orgBtns { + display: flex; + * { + margin-right: 10px; + } + } } diff --git a/src/components/Profile/Profile.tsx b/src/components/Profile/Profile.tsx index 1aaaa84..d668773 100644 --- a/src/components/Profile/Profile.tsx +++ b/src/components/Profile/Profile.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import APIService from '../../utils/ApiService'; import Navbar from '../Navbar/Navbar'; import './Profile.scss'; @@ -9,12 +9,14 @@ import { useLoggedIn, } from '../../providers/UserProvider'; import MyFiles from '../MyFiles/MyFiles'; +import { useShowChoiceNotification } from '../../providers/NotificationProvider'; /** * The profile page component */ function Profile() { const navigate = useNavigate(); + const showChoice = useShowChoiceNotification(); const getUserName = useGetUsername(); const isLoggedIn = useLoggedIn(); @@ -22,17 +24,26 @@ function Profile() { const [username] = useState(getUserName()); const [fileNames, setFileNames] = useState([]); - const getFiles = async () => { + const [organizationName, setOrganizationName] = useState(''); + + const getFiles = () => { APIService.getFileNames(username).then((res) => { setFileNames(res); }); }; + const getOrganizationName = () => { + APIService.getOrganization(username).then((res) => { + setOrganizationName(res); + }); + }; + useEffect(() => { if (!isLoggedIn) { navigate('/'); } getFiles(); + getOrganizationName(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoggedIn, username]); @@ -44,6 +55,55 @@ function Profile() { {username.slice(0, -2)} {username.slice(-2)} +

+ MyOrganization +

+ {organizationName !== '' ? ( +

+ {organizationName} +

+ ) : ( +

+ You are not a member of any organization. +

+ )} + +
+ {organizationName !== '' ? ( + <> + {/* //TODO */} + + + + ) : ( + <> + {/* //TODO */} + + + Create New + + + )} +

MyFiles

diff --git a/src/components/UserNotification/UserNotification.tsx b/src/components/UserNotification/UserNotification.tsx index aca5a90..6a20d44 100644 --- a/src/components/UserNotification/UserNotification.tsx +++ b/src/components/UserNotification/UserNotification.tsx @@ -4,6 +4,7 @@ interface Props { heading: string; info: string; onClose: () => void; + btnText?: string; } /** @@ -12,13 +13,19 @@ interface Props { * @param info More information about the notification * @param onClose The function to call when the notification is closed */ -function UserNotification({ heading, info, onClose }: Props) { +function UserNotification({ + heading, + info, + onClose, + btnText = 'Close', +}: Props) { return ( -
+
+

{heading}

{info}

- +
); diff --git a/src/providers/NotificationProvider.tsx b/src/providers/NotificationProvider.tsx index ee0a25e..7d9040d 100644 --- a/src/providers/NotificationProvider.tsx +++ b/src/providers/NotificationProvider.tsx @@ -1,11 +1,32 @@ /* eslint-disable react-refresh/only-export-components */ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useState } from 'react'; import '../index.scss'; import UserNotification from '../components/UserNotification/UserNotification'; +import ChoiceNotification from '../components/ChoiceNotification/ChoiceNotification'; const NotificationContext = React.createContext({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - show: (_heading: string, _info: string) => {}, + showNotification: ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _heading: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _info: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _buttonText?: string + ) => {}, + showChoice: ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _heading: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _info: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _onAccept: () => void, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _onReject: () => void, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _acceptText?: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _rejectText?: string + ) => {}, }); /** @@ -19,7 +40,21 @@ export function useShowNotification() { 'useShowNotification must be used within a NotificationProvider' ); } - return context.show; + return context.showNotification; +} + +/** + * Hook to get the showChoice function + * @returns A function to show a choice notification to the user + */ +export function useShowChoiceNotification() { + const context = React.useContext(NotificationContext); + if (!context) { + throw new Error( + 'useShowChoiceNotification must be used within a NotificationProvider' + ); + } + return context.showChoice; } interface Props { @@ -31,40 +66,71 @@ interface Props { * @param children The children of the component */ export function NotificationProvider({ children }: Props) { - const [visible, setVisible] = React.useState(false); - const [heading, setHeading] = React.useState(''); - const [info, setInfo] = React.useState(''); + const [element, setElement] = useState(); + + const [visible, setVisible] = useState(false); // Display the notification - const show = (pHeading: string, pInfo: string) => { - setHeading(pHeading); - setInfo(pInfo); + const showNotification = ( + pHeading: string, + pInfo: string, + btnText = 'Close' + ) => { + setElement( + + ); + setVisible(true); + }; + + // Display a choice dialog + const showChoice = ( + pHeading: string, + pInfo: string, + pOnAccept: () => void, + pOnReject: () => void, + pAcceptText = 'Yes', + pRejectText = 'No' + ) => { + setElement( + { + pOnAccept(); + close(); + }} + onReject={() => { + pOnReject(); + close(); + }} + acceptText={pAcceptText} + rejectText={pRejectText} + > + ); setVisible(true); }; // Close the notification const close = () => { setVisible(false); - setHeading(''); - setInfo(''); }; return ( <> {children} - {visible && ( - - )} + <>{visible && element} ); } diff --git a/src/utils/ApiService.ts b/src/utils/ApiService.ts index 2db37bc..131bbfb 100644 --- a/src/utils/ApiService.ts +++ b/src/utils/ApiService.ts @@ -19,7 +19,7 @@ class APIService { username: username, password: password, }); - // TODO save token + //TODO save token if (resp.data.status) { const token = resp.data.token; console.log(token); @@ -53,6 +53,35 @@ class APIService { } } + // TODO + static async getOrganization(username: string): Promise { + try { + const url = `${this.host}/get/organization/${username}`; + const resp = await axios.get(url); + return resp.data.organization; + } catch (err) { + return ''; + } + } + + // TODO + static async createOrg( + username: string, + orgName: string + ): Promise { + try { + const url = `${this.host}/create/organization`; + const resp = await axios.post(url, { + username: username, + organization: orgName, + }); + return resp.data.status; + } catch (err) { + //TODO + return true; + } + } + // TODO static async upload( username: string,