diff --git a/tutor-assistant-web/src/app/App.tsx b/tutor-assistant-web/src/app/App.tsx index 020d526..2a77b22 100644 --- a/tutor-assistant-web/src/app/App.tsx +++ b/tutor-assistant-web/src/app/App.tsx @@ -6,13 +6,15 @@ import { BrowserRouter } from 'react-router-dom' import { JoyTheme } from './JoyTheme' import { configureI18n } from './config/i18n-config.ts' import { Main } from './MainStyle.tsx' -import { HStack, MainContent } from '../lib/components/flex-layout.tsx' +import { HStack, MainContent } from '../common/components/containers/flex-layout.tsx' import { CalendarBar } from '../modules/calendar/components/CalendarBar.tsx' configureI18n(texts) - -const App = () => { +/** + * Configures routing and theme; renders CalendarBar for global access and Routing + */ +export function App() { return ( @@ -30,5 +32,3 @@ const App = () => { ) } - -export default App diff --git a/tutor-assistant-web/src/app/JoyTheme.tsx b/tutor-assistant-web/src/app/JoyTheme.tsx index 8561605..3bfec63 100644 --- a/tutor-assistant-web/src/app/JoyTheme.tsx +++ b/tutor-assistant-web/src/app/JoyTheme.tsx @@ -1,7 +1,11 @@ import * as React from 'react' import { CssVarsProvider } from '@mui/joy/styles' -import { ChildrenProps } from '../lib/types.ts' +import { ChildrenProps } from '../common/types.ts' +/** + * Provides the base theme for the app + * @param children to be rendered + */ export function JoyTheme({ children }: ChildrenProps) { return ( diff --git a/tutor-assistant-web/src/app/MainStyle.tsx b/tutor-assistant-web/src/app/MainStyle.tsx index 6ea0785..75f5654 100644 --- a/tutor-assistant-web/src/app/MainStyle.tsx +++ b/tutor-assistant-web/src/app/MainStyle.tsx @@ -1,5 +1,8 @@ import { Box, styled } from '@mui/joy' +/** + * Applies global styling according to the theme + */ export const Main = styled(Box)` width: 100%; height: 100%; diff --git a/tutor-assistant-web/src/app/auth/Auth.tsx b/tutor-assistant-web/src/app/auth/Auth.tsx index 1a7fc4a..946f57a 100644 --- a/tutor-assistant-web/src/app/auth/Auth.tsx +++ b/tutor-assistant-web/src/app/auth/Auth.tsx @@ -1,7 +1,7 @@ import { createContext } from 'react' import axios, { AxiosInstance } from 'axios' -import { chill, isNotPresent } from '../../lib/utils/utils.ts' -import { ChildrenProps } from '../../lib/types.ts' +import { chill, isNotPresent } from '../../common/utils/utils.ts' +import { ChildrenProps } from '../../common/types.ts' import { useKeycloak } from '@react-keycloak/web' type AuthContextType = { @@ -12,6 +12,16 @@ type AuthContextType = { getRoles: () => string[] } +/** + * Use only through useAuth + * + * Provides: + * getAuthHttp: returns a REST client with authorization headers set if the user is logged in + * isLoggedIn: if the user is logged in. + * openLogin: opens the login page of the identity provider. + * logout: logouts the user + * getRoles: returns the users roles + */ export const AuthContext = createContext({ getAuthHttp: () => axios, isLoggedIn: () => false, @@ -21,6 +31,11 @@ export const AuthContext = createContext({ }) +/** + * Applies AuthContext + * + * @param children wrapped by this context provider + */ export function Auth({ children }: ChildrenProps) { const { keycloak, initialized } = useKeycloak() diff --git a/tutor-assistant-web/src/app/auth/Authenticated.tsx b/tutor-assistant-web/src/app/auth/Authenticated.tsx index 675e3db..97195c2 100644 --- a/tutor-assistant-web/src/app/auth/Authenticated.tsx +++ b/tutor-assistant-web/src/app/auth/Authenticated.tsx @@ -1,12 +1,18 @@ -import { ChildrenProps } from '../../lib/types.ts' +import { ChildrenProps } from '../../common/types.ts' import { useAuth } from './useAuth.ts' -import { isPresent } from '../../lib/utils/utils.ts' -import { haveCommonElements } from '../../lib/utils/array-utils.ts' +import { isPresent } from '../../common/utils/utils.ts' +import { haveCommonElements } from '../../common/utils/array-utils.ts' interface Props extends ChildrenProps { roles?: string[] } +/** + * Protects a component from rendering based on the users authentication and authorization + * + * @param children rendered iff the user is logged in and has the required roles + * @param roles the user must have in order to render the children + */ export function Authenticated({ children, roles }: Props) { const { isLoggedIn, openLogin, getRoles } = useAuth() diff --git a/tutor-assistant-web/src/app/auth/useAuth.ts b/tutor-assistant-web/src/app/auth/useAuth.ts index e4193bb..4606af3 100644 --- a/tutor-assistant-web/src/app/auth/useAuth.ts +++ b/tutor-assistant-web/src/app/auth/useAuth.ts @@ -1,6 +1,9 @@ import { useContext } from 'react' import { AuthContext } from './Auth.tsx' +/** + * Provides common authentication functionality and common functionality that requires authentication + */ export function useAuth() { - return useContext(AuthContext) + return useContext(AuthContext) } diff --git a/tutor-assistant-web/src/app/base.ts b/tutor-assistant-web/src/app/base.ts index 12510d5..858af47 100644 --- a/tutor-assistant-web/src/app/base.ts +++ b/tutor-assistant-web/src/app/base.ts @@ -1,9 +1,18 @@ -import { getCurrentBaseUrl } from '../lib/utils/utils.ts' +import { getCurrentBaseUrl } from '../common/utils/utils.ts' const envApiBaseUrl = import.meta.env.VITE_API_BASE_URL as string const envKeycloakBaseUrl = import.meta.env.VITE_KEYCLOAK_BASE_URL as string const currentBaseUrl = getCurrentBaseUrl() +/** + * Backend api url the web frontend is supposed to communicate with + * Must be configured through env. 'default' uses the same protocol and host as this web frontend + */ export const apiBaseUrl = envApiBaseUrl !== 'default' ? envApiBaseUrl : `${currentBaseUrl}/api` + +/** + * Keycloak url for authentication and authorization + * Must be configured through env. 'default' uses the same protocol and host as this web frontend + */ export const keycloakBaseUrl = envKeycloakBaseUrl !== 'default' ? envKeycloakBaseUrl : `${currentBaseUrl}/auth` diff --git a/tutor-assistant-web/src/app/config/i18n-config.ts b/tutor-assistant-web/src/app/config/i18n-config.ts index d2ed291..7a5427a 100644 --- a/tutor-assistant-web/src/app/config/i18n-config.ts +++ b/tutor-assistant-web/src/app/config/i18n-config.ts @@ -2,31 +2,44 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' import LanguageDetector from 'i18next-browser-languagedetector' +/** + * Applies internationalization + * + * @param texts to be translated, requires the format: + * { + * textKey1: { + * "en": "value", + * "de": "value", + * ... + * }, + * ... + * } + */ export function configureI18n(texts: any) { - function restructureTranslations(texts: any) { - const result = {} as any + function restructureTranslations(texts: any) { + const result = {} as any - Object.keys(texts).forEach(key => { - Object.keys(texts[key]).forEach(lang => { - if (!result[lang]) { - result[lang] = { translation: {} } - } - result[lang].translation[key] = texts[key][lang] - }) - }) + Object.keys(texts).forEach(key => { + Object.keys(texts[key]).forEach(lang => { + if (!result[lang]) { + result[lang] = { translation: {} } + } + result[lang].translation[key] = texts[key][lang] + }) + }) - return result - } + return result + } - i18n - .use(initReactI18next) - .use(LanguageDetector) - .init({ - resources: restructureTranslations(texts), - fallbackLng: 'en', - interpolation: { - escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape - }, - }) + i18n + .use(initReactI18next) + .use(LanguageDetector) + .init({ + resources: restructureTranslations(texts), + fallbackLng: 'en', + interpolation: { + escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape + }, + }) } diff --git a/tutor-assistant-web/src/app/config/keycloak-config.ts b/tutor-assistant-web/src/app/config/keycloak-config.ts index 3f97d2d..6fea706 100644 --- a/tutor-assistant-web/src/app/config/keycloak-config.ts +++ b/tutor-assistant-web/src/app/config/keycloak-config.ts @@ -1,6 +1,9 @@ import Keycloak from 'keycloak-js' import { keycloakBaseUrl } from '../base.ts' +/** + * Returns configuration for accessing keycloak + */ export const keycloak = new Keycloak({ url: keycloakBaseUrl, realm: 'tutor-assistant', diff --git a/tutor-assistant-web/src/app/routing/Routing.tsx b/tutor-assistant-web/src/app/routing/Routing.tsx index 8eaa65e..877faaf 100644 --- a/tutor-assistant-web/src/app/routing/Routing.tsx +++ b/tutor-assistant-web/src/app/routing/Routing.tsx @@ -2,14 +2,13 @@ import { Navigate, Route, Routes } from 'react-router-dom' import { ChatPage } from '../../modules/chat/ChatPage.tsx' import { Authenticated } from '../auth/Authenticated.tsx' import { DocumentsPage } from '../../modules/documents/DocumentsPage.tsx' -import { HelperPage } from '../../modules/helper/HelperPage.tsx' import { ChatProvider } from '../../modules/chat/ChatProvider.tsx' -interface Props { - -} - -export function Routing({}: Props) { +/** + * Configures routing + * Sub routes might be configured in child components + */ +export function Routing() { return ( @@ -34,14 +33,6 @@ export function Routing({}: Props) { } /> - - - - } - /> - ) } diff --git a/tutor-assistant-web/src/common/components/Bar.tsx b/tutor-assistant-web/src/common/components/containers/Bar.tsx similarity index 69% rename from tutor-assistant-web/src/common/components/Bar.tsx rename to tutor-assistant-web/src/common/components/containers/Bar.tsx index 1e0d210..af8f6ac 100644 --- a/tutor-assistant-web/src/common/components/Bar.tsx +++ b/tutor-assistant-web/src/common/components/containers/Bar.tsx @@ -1,7 +1,13 @@ import { styled } from '@mui/joy' -import { VStack } from '../../lib/components/flex-layout.tsx' +import { VStack } from './flex-layout.tsx' const barWidth = '380px' + +/** + * Sidebar with surface background based on a VStack + * + * Supports className 'right' to change styling when used as a bar on the right + */ export const Bar = styled(VStack)` min-width: ${barWidth}; width: ${barWidth}; diff --git a/tutor-assistant-web/src/lib/components/ColumnLayout.tsx b/tutor-assistant-web/src/common/components/containers/ColumnLayout.tsx similarity index 87% rename from tutor-assistant-web/src/lib/components/ColumnLayout.tsx rename to tutor-assistant-web/src/common/components/containers/ColumnLayout.tsx index 8fd37e6..bb2ee4d 100644 --- a/tutor-assistant-web/src/lib/components/ColumnLayout.tsx +++ b/tutor-assistant-web/src/common/components/containers/ColumnLayout.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' -import { byNumberReverse, isNotPresent, stringToNumber } from '../utils/utils.ts' -import { last } from '../utils/array-utils.ts' +import { byNumberReverse, isNotPresent, stringToNumber } from '../../utils/utils.ts' +import { last } from '../../utils/array-utils.ts' import { HStack, MainContent, VStack } from './flex-layout.tsx' import { Scroller } from './Scroller.tsx' @@ -14,6 +14,17 @@ interface Props { spacing?: number } +/** + * Renders items in a column layout + * + * @param columnCounts defines the number of columns absolutely or dependent of the window width (in px): + * Examples: 3, {0: 1, 400: 2, 800: 3} + * @param fill either vertical-wise or horizontal-wise + * @param values to be rendered + * @param render applied on each value + * @param spacing between the rendered items. Defaults to 0 + * @constructor + */ export function ColumnLayout({ columnCounts, fill, values, render, spacing }: Props) { if (isNotPresent(spacing)) spacing = 0 diff --git a/tutor-assistant-web/src/common/components/Header.tsx b/tutor-assistant-web/src/common/components/containers/Header.tsx similarity index 69% rename from tutor-assistant-web/src/common/components/Header.tsx rename to tutor-assistant-web/src/common/components/containers/Header.tsx index b44657b..5c9024d 100644 --- a/tutor-assistant-web/src/common/components/Header.tsx +++ b/tutor-assistant-web/src/common/components/containers/Header.tsx @@ -1,7 +1,7 @@ -import { Row, Spacer } from '../../lib/components/flex-layout.tsx' +import { Row, Spacer } from './flex-layout.tsx' import { Divider, Typography } from '@mui/joy' import React, { ReactNode } from 'react' -import { isPresent } from '../../lib/utils/utils.ts' +import { isPresent } from '../../utils/utils.ts' interface Props { title: ReactNode | string @@ -9,6 +9,14 @@ interface Props { rightNode?: ReactNode } + +/** + * Horizontal bar to render a title with additional items at the top + * + * @param title rendered in the middle of the bar + * @param leftNode rendered at the left border + * @param rightNode rendered at the right border + */ export function Header({ title, leftNode, rightNode }: Props) { return ( <> diff --git a/tutor-assistant-web/src/lib/components/Scroller.tsx b/tutor-assistant-web/src/common/components/containers/Scroller.tsx similarity index 72% rename from tutor-assistant-web/src/lib/components/Scroller.tsx rename to tutor-assistant-web/src/common/components/containers/Scroller.tsx index 876ba63..1f055ba 100644 --- a/tutor-assistant-web/src/lib/components/Scroller.tsx +++ b/tutor-assistant-web/src/common/components/containers/Scroller.tsx @@ -1,7 +1,7 @@ import { Box } from '@mui/joy' import { ReactNode, useEffect, useRef } from 'react' -import { isNotPresent } from '../utils/utils.ts' -import { empty } from '../utils/array-utils.ts' +import { isNotPresent } from '../../utils/utils.ts' +import { empty } from '../../utils/array-utils.ts' interface Props { children: ReactNode @@ -9,6 +9,14 @@ interface Props { scrollToBottomOnChange?: unknown[] } +/** + * Renders items in a vertically scrollable container + * + * @param children to be rendered inside this wrapper + * @param padding between this wrapper and its content + * @param scrollToBottomOnChange dependencies on whose change to scroll down to the bottom + * @constructor + */ export function Scroller({ children, padding, scrollToBottomOnChange }: Props) { if (isNotPresent(scrollToBottomOnChange)) scrollToBottomOnChange = [] diff --git a/tutor-assistant-web/src/lib/components/flex-layout.tsx b/tutor-assistant-web/src/common/components/containers/flex-layout.tsx similarity index 67% rename from tutor-assistant-web/src/lib/components/flex-layout.tsx rename to tutor-assistant-web/src/common/components/containers/flex-layout.tsx index 61df386..cc3554f 100644 --- a/tutor-assistant-web/src/lib/components/flex-layout.tsx +++ b/tutor-assistant-web/src/common/components/containers/flex-layout.tsx @@ -1,5 +1,8 @@ import { Box, Stack, styled } from '@mui/joy' +/** + * Full-width, full-height vertical stack with flex layout + */ export const VStack = styled(Stack)` margin: 0 !important; flex-direction: column; @@ -8,6 +11,9 @@ export const VStack = styled(Stack)` overflow-y: hidden; ` +/** + * Full-width, full-height horizontal stack with flex layout + */ export const HStack = styled(Stack)` margin: 0 !important; flex-direction: row; @@ -23,12 +29,18 @@ export const Column = styled(Stack)` overflow-y: hidden; ` +/** + * Full-width, min-height horizontal stack with flex layout + */ export const Row = styled(Stack)` margin: 0 !important; flex-direction: row; width: 100%; ` +/** + * Extending content inside a flex container + */ export const MainContent = styled(Box)` width: 100%; height: 100%; @@ -36,6 +48,9 @@ export const MainContent = styled(Box)` overflow: hidden; ` +/** + * Creating empty space between items in a flex container in order to create maximum space between items + */ export const Spacer = styled('span')` flex: 1; ` diff --git a/tutor-assistant-web/src/modules/documents/components/FileButton.tsx b/tutor-assistant-web/src/common/components/widgets/FileButton.tsx similarity index 85% rename from tutor-assistant-web/src/modules/documents/components/FileButton.tsx rename to tutor-assistant-web/src/common/components/widgets/FileButton.tsx index 6e7f592..8dd976c 100644 --- a/tutor-assistant-web/src/modules/documents/components/FileButton.tsx +++ b/tutor-assistant-web/src/common/components/widgets/FileButton.tsx @@ -1,11 +1,17 @@ import { Button, ButtonProps, styled } from '@mui/joy' import React, { ChangeEvent, useRef } from 'react' -import { isNotPresent } from '../../../lib/utils/utils.ts' +import { isNotPresent } from '../../utils/utils.ts' interface Props { addFile: (file: File) => void } +/** + * Button for selecting files + * + * @param addFile callback to be called if a file is selected + * @param props of the Button + */ export function FileButton({ addFile, ...props }: Props & ButtonProps) { const fileInputRef = useRef(null) diff --git a/tutor-assistant-web/src/lib/components/Multiline.tsx b/tutor-assistant-web/src/common/components/widgets/Multiline.tsx similarity index 83% rename from tutor-assistant-web/src/lib/components/Multiline.tsx rename to tutor-assistant-web/src/common/components/widgets/Multiline.tsx index d7ccb76..7a03825 100644 --- a/tutor-assistant-web/src/lib/components/Multiline.tsx +++ b/tutor-assistant-web/src/common/components/widgets/Multiline.tsx @@ -5,6 +5,14 @@ interface Props { text: string } + +/** + * Renders \n as
+ * + * @param text to be rendered + * @param props of Typography + * @constructor + */ export function Multiline({ text, ...props }: Props & TypographyProps) { const lines = text.split('\n') const maxIndex = lines.length - 1 diff --git a/tutor-assistant-web/src/lib/components/StarRater.tsx b/tutor-assistant-web/src/common/components/widgets/StarRater.tsx similarity index 76% rename from tutor-assistant-web/src/lib/components/StarRater.tsx rename to tutor-assistant-web/src/common/components/widgets/StarRater.tsx index efec078..c5e5688 100644 --- a/tutor-assistant-web/src/lib/components/StarRater.tsx +++ b/tutor-assistant-web/src/common/components/widgets/StarRater.tsx @@ -1,8 +1,8 @@ -import { Row } from './flex-layout.tsx' -import { range } from '../utils/array-utils.ts' +import { Row } from '../containers/flex-layout.tsx' +import { range } from '../../utils/array-utils.ts' import { useState } from 'react' import { Star, StarOutline } from '@mui/icons-material' -import { isPresent } from '../utils/utils.ts' +import { isPresent } from '../../utils/utils.ts' interface Props { max: number @@ -10,6 +10,16 @@ interface Props { onSelect: (rating: number) => void } +/** + * Picker of star icons + * + * Controlled component + * + * @param max number of stars + * @param rating currently selected. Takes values from 1 to max (both inclusive) + * @param onSelect function called when a star is clicked. Takes the rating + * @constructor + */ export function StarRater({ max, rating, onSelect }: Props) { const [preview, setPreview] = useState() diff --git a/tutor-assistant-web/src/common/components/StyledDivider.tsx b/tutor-assistant-web/src/common/components/widgets/StyledDivider.tsx similarity index 64% rename from tutor-assistant-web/src/common/components/StyledDivider.tsx rename to tutor-assistant-web/src/common/components/widgets/StyledDivider.tsx index 18587d8..0b380c6 100644 --- a/tutor-assistant-web/src/common/components/StyledDivider.tsx +++ b/tutor-assistant-web/src/common/components/widgets/StyledDivider.tsx @@ -1,5 +1,8 @@ import { Divider, styled } from '@mui/joy' +/** + * Divider according to the theme with additional styling + */ export const StyledDivider = styled(Divider)` margin-top: 0 !important; ` diff --git a/tutor-assistant-web/src/common/components/StyledMarkdown.tsx b/tutor-assistant-web/src/common/components/widgets/StyledMarkdown.tsx similarity index 80% rename from tutor-assistant-web/src/common/components/StyledMarkdown.tsx rename to tutor-assistant-web/src/common/components/widgets/StyledMarkdown.tsx index 3a94dd8..e8e397c 100644 --- a/tutor-assistant-web/src/common/components/StyledMarkdown.tsx +++ b/tutor-assistant-web/src/common/components/widgets/StyledMarkdown.tsx @@ -1,6 +1,9 @@ import { styled } from '@mui/joy' import Markdown from 'react-markdown' +/** + * Styling Markdown according to the theme + */ export const StyledMarkdown = styled(Markdown)` code.hljs { background: ${props => props.theme.palette.background.surface}; diff --git a/tutor-assistant-web/src/common/components/SubmitTextarea.tsx b/tutor-assistant-web/src/common/components/widgets/SubmitTextarea.tsx similarity index 56% rename from tutor-assistant-web/src/common/components/SubmitTextarea.tsx rename to tutor-assistant-web/src/common/components/widgets/SubmitTextarea.tsx index 27a91ae..404ec1e 100644 --- a/tutor-assistant-web/src/common/components/SubmitTextarea.tsx +++ b/tutor-assistant-web/src/common/components/widgets/SubmitTextarea.tsx @@ -1,20 +1,27 @@ import { Button, Textarea, TextareaProps, Tooltip } from '@mui/joy' import React, { ReactNode } from 'react' -import { Row, Spacer } from '../../lib/components/flex-layout.tsx' +import { Row, Spacer } from '../containers/flex-layout.tsx' import { useTranslation } from 'react-i18next' interface Props { - onCtrlEnter: () => void + onSubmit: () => void additionEndDecorator?: ReactNode } -export function SubmitTextarea({ onCtrlEnter, additionEndDecorator, ...props }: TextareaProps & Props) { +/** + * Makes a Textarea submit on Ctrl + Enter and adds a submit button at the right border of the end decorator + * + * @param onSubmit action to be performed on Ctrl + Enter or on submit button clicked + * @param additionEndDecorator react node to be rendered at the left border of the endDecorator + * @param props of the Textarea + */ +export function SubmitTextarea({ onSubmit, additionEndDecorator, ...props }: TextareaProps & Props) { const { t } = useTranslation() function handleInput(e: React.KeyboardEvent) { if (e.ctrlKey && e.key === 'Enter') { e.preventDefault() - onCtrlEnter() + onSubmit() } } @@ -27,7 +34,7 @@ export function SubmitTextarea({ onCtrlEnter, additionEndDecorator, ...props }: {additionEndDecorator} - + diff --git a/tutor-assistant-web/src/common/types.ts b/tutor-assistant-web/src/common/types.ts new file mode 100644 index 0000000..9babeee --- /dev/null +++ b/tutor-assistant-web/src/common/types.ts @@ -0,0 +1,5 @@ +import { ReactNode } from 'react' + +export interface ChildrenProps { + children?: ReactNode; +} diff --git a/tutor-assistant-web/src/common/utils/array-utils.ts b/tutor-assistant-web/src/common/utils/array-utils.ts new file mode 100644 index 0000000..a492573 --- /dev/null +++ b/tutor-assistant-web/src/common/utils/array-utils.ts @@ -0,0 +1,129 @@ +import { isNotPresent } from './utils.ts' + +/** + * Appends an item to the end of an array + * + * Pure function + * @param item to append + * @param array to updated + * @returns a new array with the new item + */ +export function append(item: T, array: T[]) { + return [...array, item] +} + +/** + * Updates an object with an id in an array + * + * Finds the object to update by the id + * + * Pure function + * @param item to update + * @param array to update + * @returns a new array with the item updated if the id is present else the original array + */ +export function update(item: T, array: T[]) { + if (isNotPresent(item.id)) return array + return array.map(item => item.id === item.id ? item : item) +} + +/** + * Removes an object with an id in an array + * + * Pure function + * @param item either an id or an object with an id to be removed + * @param array to update + * @returns a new array without the object with the given id + */ +export function remove(item: T | string, array: T[]) { + const id = typeof item === 'string' ? item : item.id + return array.filter(it => it.id !== id) +} + +/** + * last index of a given array + * + * Pure function + * @param array + * @returns the last index of the array or -1 + */ +export function lastIndex(array: any[]) { + return array.length - 1 +} + +/** + * last element of a given array + * + * Pure function + * @param array + * @returns last element of the array or undefined + */ +export function last(array: T[]) { + if (empty(array)) return undefined + return array[lastIndex(array)] +} + +/** + * checks if a given array has no elements + * + * Pure function + * @param array + * @returns if the array is empty + */ +export function empty(array: unknown[]) { + return array.length === 0 +} + +export function notEmpty(array: unknown[]) { + return !empty(array) +} + + +/** + * Partitions an array into two arrays + * + * Pure function + * @param predicate to partition the array + * @param array to be partitioned + * @returns an array with two elements + * The first array contains all elements of the array for which predicate returns true, + * the second array contains all other elements + */ +export function partition(predicate: (item: T) => boolean, array: T[]) { + const result = [[], []] as [T[], T[]] + array.forEach(item => predicate(item) ? result[0].push(item) : result[1].push(item)) + return result +} + +/** + * Checks, if two arrays have at least one common element + * + * Pure function + * @param array1 first array + * @param array2 second array + * @returns true if the arrays have at least one common array else false + */ +export function haveCommonElements(array1: T[], array2: T[]): boolean { + const set1 = new Set(array1) + for (const item of array2) { + if (set1.has(item)) { + return true + } + } + return false +} + +/** + * Creates an array with all integers between the given integers in ascending order + * + * @param start integer inclusive + * @param end integer exclusive + * @returns an array with integers from start to end + */ +export function range(start: number, end: number) { + let result = [] + for (let i = start; i < end; i++) { + result.push(i) + } + return result +} diff --git a/tutor-assistant-web/src/common/utils/math-utils.ts b/tutor-assistant-web/src/common/utils/math-utils.ts new file mode 100644 index 0000000..d411155 --- /dev/null +++ b/tutor-assistant-web/src/common/utils/math-utils.ts @@ -0,0 +1,10 @@ +/** + * Rounds a value to decimals + * + * @param value to be rounded + * @param decimals to which the value is supposed to be rounded + */ +export function roundTo(value: number, decimals: number) { + const factor = Math.pow(10, decimals) + return Math.round(value * factor) / factor +} \ No newline at end of file diff --git a/tutor-assistant-web/src/common/utils/utils.ts b/tutor-assistant-web/src/common/utils/utils.ts new file mode 100644 index 0000000..e4c741e --- /dev/null +++ b/tutor-assistant-web/src/common/utils/utils.ts @@ -0,0 +1,50 @@ +/** + * @param value to check + * @returns if the given value is null or undefined + */ +export function isNotPresent(value: any): value is null | undefined { + return value === undefined || value === null +} + +/** + * @param value to check + * @returns if the given value is of its type but not undefined or null + */ +export function isPresent(value: T | undefined | null): value is T { + return !isNotPresent(value) +} + +/** + * Sorting value for revered soring + * + * @param a first value + * @param b second value + * @returns positive if a is less than b, negative if a is greater than b, zero if they are equal + */ +export function byNumberReverse(a: number, b: number) { + return b - a +} + +/** + * Converts a string to a number + * + * @param s to be converted + * @returns a number of the given string + */ +export function stringToNumber(s: string) { + return +s +} + +/** + * @returns this base url + */ +export function getCurrentBaseUrl() { + const { protocol, host } = window.location + return `${protocol}//${host}` +} + +/** + * Function that does nothing + * Can be used as default value + */ +export const chill = () => undefined diff --git a/tutor-assistant-web/src/index.css b/tutor-assistant-web/src/index.css index 356388f..ea9b311 100644 --- a/tutor-assistant-web/src/index.css +++ b/tutor-assistant-web/src/index.css @@ -29,10 +29,3 @@ div#root { height: 100%; overflow-y: hidden; } - -/* -display: flex; -place-items: center; -*/ - - diff --git a/tutor-assistant-web/src/lib/components/BaseLayout.tsx b/tutor-assistant-web/src/lib/components/BaseLayout.tsx deleted file mode 100644 index 59dcf9b..0000000 --- a/tutor-assistant-web/src/lib/components/BaseLayout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import {VStack} from "./flex-layout.tsx"; - -interface Props { - -} - -export function BaseLayout({}: Props) { - return ( - - - - ) -} diff --git a/tutor-assistant-web/src/lib/hooks/useCallOnce.ts b/tutor-assistant-web/src/lib/hooks/useCallOnce.ts deleted file mode 100644 index 72e1a51..0000000 --- a/tutor-assistant-web/src/lib/hooks/useCallOnce.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { EffectCallback, useEffect, useRef } from 'react' - -export function useCallOnce(callback: EffectCallback) { - const called = useRef(false) - useEffect(() => { - if (called.current) return - called.current = true - return callback() - }, []) -} diff --git a/tutor-assistant-web/src/lib/hooks/useInstantState.ts b/tutor-assistant-web/src/lib/hooks/useInstantState.ts deleted file mode 100644 index e52946a..0000000 --- a/tutor-assistant-web/src/lib/hooks/useInstantState.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useRef, useState } from 'react' - - -export function useInstantState(initial: T): [() => T, (value: T) => void] { - const [_, setState] = useState(initial) - const stateRef = useRef(initial) - - function setInstantState(value: T) { - stateRef.current = value - setState(value) - } - - function getInstantState() { - return stateRef.current - } - - return [getInstantState, setInstantState] -} diff --git a/tutor-assistant-web/src/lib/hooks/useOnChange.ts b/tutor-assistant-web/src/lib/hooks/useOnChange.ts deleted file mode 100644 index 8805db2..0000000 --- a/tutor-assistant-web/src/lib/hooks/useOnChange.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useRef } from 'react' - -export function useOnChange(callback: () => void, values: any[]) { - const prevRef = useRef([]) - - function hasChanged() { - const prev = prevRef.current - - if (prev.length !== values.length) { - return true - } - - for (let i = 0; i < prev.length; i++) { - if (values[i] !== prev[i]) { - return true - } - } - - return false - } - - if (hasChanged()) { - prevRef.current = values - callback() - } -} diff --git a/tutor-assistant-web/src/lib/types.ts b/tutor-assistant-web/src/lib/types.ts deleted file mode 100644 index d988669..0000000 --- a/tutor-assistant-web/src/lib/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Dispatch, FormEvent, ReactNode, SetStateAction } from 'react' - -export interface ChildrenProps { - children?: ReactNode; -} - -export type HTMLFormEvent = FormEvent; -export type DivMouseEvent = React.MouseEvent -export type StateCalculator = (prev: T) => T -export type StateChanger = (changer: StateCalculator) => void -export type StateSetter = React.Dispatch> -export type State = [S | undefined, Dispatch>] diff --git a/tutor-assistant-web/src/lib/utils/array-utils.ts b/tutor-assistant-web/src/lib/utils/array-utils.ts deleted file mode 100644 index 44dc08e..0000000 --- a/tutor-assistant-web/src/lib/utils/array-utils.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { isNotPresent } from './utils' -import { isBetweenExcluded, isBetweenIncluded } from './math-utils' - -export function append(item: T, array: T[]) { - return [...array, item] -} - -export function update(item: T, array: T[]) { - if (isNotPresent(item.id)) return array - return array.map(item => item.id === item.id ? item : item) -} - -export function remove(item: T | string, array: T[]) { - const id = typeof item === 'string' ? item : item.id - return array.filter(it => it.id !== id) -} - -export function lastIndex(array: any[]) { - return array.length - 1 -} - -export function last(array: T[]) { - if (empty(array)) return undefined - return array[lastIndex(array)] -} - -export function empty(array: unknown[]) { - return array.length === 0 -} - -export function notEmpty(array: unknown[]) { - return !empty(array) -} - -export function partition(predicate: (item: T) => boolean, array: T[]) { - const result = [[], []] as [T[], T[]] - array.forEach(item => predicate(item) ? result[0].push(item) : result[1].push(item)) - return result -} - -export function haveCommonElements(array1: T[], array2: T[]): boolean { - const set1 = new Set(array1) - for (const item of array2) { - if (set1.has(item)) { - return true - } - } - return false -} - -export function pairwise(array: T[], func: (current: T, next: T) => T) { - const result = [] - for (let i = 0; i < array.length - 1; i++) { - result.push(func(array[i], array[i + 1])) - } - return result -} - -export function pairwiseKeepingFirst(array: T[], func: (current: T, next: T) => T) { - if (array.length === 0) return [] - const result = [array[0]] - - return result.push(...pairwise(array, func)) -} - -export function pairwiseKeepingFirstInstantlyApplied( - array: T[], func: (current: T, next: T) => T) { - - const arrayCopy = [...array] - - if (arrayCopy.length === 0) return [] - - for (let i = 0; i < arrayCopy.length - 1; i++) { - arrayCopy[i] = (func(arrayCopy[i], arrayCopy[i + 1])) - } - return arrayCopy -} - -export function convertN(array: T[], n: number, func: (nItems: T[]) => R) { - const result: R[] = [] - - if (array.length % n !== 0) return result - - let nItems: T[] = [] - for (let i = 0; i < array.length; i++) { - nItems.push(array[i]) - - if (nItems.length % n === 0) { - result.push(func(nItems)) - nItems = [] - } - } - return result -} - -export function range(start: number, end: number) { - let result = [] - for (let i = start; i < end; i++) { - result.push(i) - } - return result -} - -export function getBestFit(value: number, array: T[], mapToNumber: (item: T) => number) { - function continueSearchIndex(index: number) { - return isBetweenExcluded(0, index, length - 1) && - !isBetweenIncluded(mapToNumber(array[index - 1]), value, mapToNumber(array[index])) - } - - function calculateNextIndex(a: number, b: number) { - return Math.floor((a + b) / 2) - } - - const length = array.length - - if (length === 0) throw Error('Array length must not be 0') - if (length === 1) return array[0] - - let lowerBound = 0 - let upperBound = length - 1 - let index = calculateNextIndex(upperBound, lowerBound) - let prevIndex = Infinity - - while (isBetweenExcluded(0, index, length - 1) && prevIndex !== index) { - - const current = mapToNumber(array[index]) - if (value < current) { - upperBound = index - } else if (value > current) { - lowerBound = index - } else { - return array[index] - } - - prevIndex = index - index = calculateNextIndex(lowerBound, upperBound) - } - - const nextIndex = index + 1 - const nextIndexValueDiff = mapToNumber(array[nextIndex]) - value - const indexValueDiff = value - mapToNumber(array[index]) - - return nextIndexValueDiff < indexValueDiff ? array[nextIndex] : array[index] -} diff --git a/tutor-assistant-web/src/lib/utils/math-utils.ts b/tutor-assistant-web/src/lib/utils/math-utils.ts deleted file mode 100644 index de5e10b..0000000 --- a/tutor-assistant-web/src/lib/utils/math-utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const randomId = () => - Math.random().toString().replace('.', '') - -export function isBetweenIncluded(lower: number, value: number, upper: number) { - return isBetweenExcluded(lower - 1, value, upper + 1) -} - -export function isBetweenExcluded(lower: number, value: number, upper: number) { - return lower < value && value < upper -} - -export function roundTo(value: number, decimals: number) { - const factor = Math.pow(10, decimals) - return Math.round(value * factor) / factor -} \ No newline at end of file diff --git a/tutor-assistant-web/src/lib/utils/scroll-utils.ts b/tutor-assistant-web/src/lib/utils/scroll-utils.ts deleted file mode 100644 index d47ac42..0000000 --- a/tutor-assistant-web/src/lib/utils/scroll-utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { isNotPresent, isPresent } from './utils' - -export function scrollDown(element: HTMLElement | null) { - const content = getScrollContainer(element) - if (isNotPresent(content)) return - content.scroll({ - top: content.scrollHeight, - behavior: 'smooth' - }) -} - -export function getScrollContainer(element: HTMLElement | null) { - let currentElement = element - - while (isPresent(currentElement)) { - if (currentElement.classList.contains('content-scroller')) { - return currentElement - } - - currentElement = currentElement.parentElement - } -} diff --git a/tutor-assistant-web/src/lib/utils/ui-utils.ts b/tutor-assistant-web/src/lib/utils/ui-utils.ts deleted file mode 100644 index e96f06c..0000000 --- a/tutor-assistant-web/src/lib/utils/ui-utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -export function isHslColorLightOrDark(h: number, s: number, l: number): boolean { - let r: number, g: number, b: number - if (s === 0) { - r = g = b = l // achromatic - } else { - const hue2rgb = (p: number, q: number, t: number): number => { - if (t < 0) t += 1 - if (t > 1) t -= 1 - if (t < 1 / 6) return p + (q - p) * 6 * t - if (t < 1 / 2) return q - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 - return p - } - const q = l < 0.5 ? l * (1 + s) : l + s - l * s - const p = 2 * l - q - r = hue2rgb(p, q, h + 1 / 3) - g = hue2rgb(p, q, h) - b = hue2rgb(p, q, h - 1 / 3) - } - - const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b - - return luminance > 0.5 -} - -export function parseHSL(hsl: string): [number, number, number] | undefined { - const match = hsl.match(/^hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)$/) - if (match) { - const h = parseFloat(match[1]) / 360 - const s = parseFloat(match[2]) / 100 - const l = parseFloat(match[3]) / 100 - return [h, s, l] - } -} diff --git a/tutor-assistant-web/src/lib/utils/utils.ts b/tutor-assistant-web/src/lib/utils/utils.ts deleted file mode 100644 index afc0b39..0000000 --- a/tutor-assistant-web/src/lib/utils/utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -export function isNotPresent(value: any): value is null | undefined { - return value === undefined || value === null -} - -export function isPresent(value: T | undefined | null): value is T { - return !isNotPresent(value) -} - -export const postDecode = (object: any) => { - const mutObject = { ...object } - for (const key in mutObject) { - if (typeof mutObject[key] === object) { - mutObject[key] = postDecode(mutObject[key]) - } - if (key.endsWith('Date')) { - mutObject[key] = new Date(key) - } - } - return mutObject -} - -export function getNavigationLocation() { - return window.location.href.substring(window.location.origin.length) -} - -export function byNumber(a: number, b: number) { - return a - b -} - -export function byNumberReverse(a: number, b: number) { - return b - a -} - -export function stringToNumber(s: string) { - return +s -} - -export function isAppleMobile() { - return /iPad|iPhone|iPod/.test(navigator.userAgent) -} - -export function getCurrentBaseUrl() { - const { protocol, host } = window.location - return `${protocol}//${host}` -} - -export const chill = () => undefined diff --git a/tutor-assistant-web/src/main.tsx b/tutor-assistant-web/src/main.tsx index 15a0d50..f3bff89 100644 --- a/tutor-assistant-web/src/main.tsx +++ b/tutor-assistant-web/src/main.tsx @@ -1,30 +1,19 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import App from './app/App.tsx' +import { App } from './app/App.tsx' import './index.css' import { ReactKeycloakProvider } from '@react-keycloak/web' import { keycloak } from './app/config/keycloak-config.ts' import '@fontsource/inter' -// import '@fontsource/inter/100.css' -// import '@fontsource/inter/200.css' -// import '@fontsource/inter/300.css' -// import '@fontsource/inter/400.css' import '@fontsource/inter/500.css' import '@fontsource/inter/600.css' import '@fontsource/inter/700.css' -// import '@fontsource/inter/800.css' -// import '@fontsource/inter/900.css' -// import '@fontsource/inter/100-italic.css' -// import '@fontsource/inter/200-italic.css' -// import '@fontsource/inter/300-italic.css' -// import '@fontsource/inter/400-italic.css' import '@fontsource/inter/500-italic.css' import '@fontsource/inter/600-italic.css' import '@fontsource/inter/700-italic.css' -// import '@fontsource/inter/800-italic.css' -// import '@fontsource/inter/900-italic.css' +// Keycloak must be outside strict mode because of side effects createRoot(document.getElementById('root')!).render( diff --git a/tutor-assistant-web/src/modules/calendar/calendar-model.ts b/tutor-assistant-web/src/modules/calendar/calendar-model.ts index 41c2f93..b266cda 100644 --- a/tutor-assistant-web/src/modules/calendar/calendar-model.ts +++ b/tutor-assistant-web/src/modules/calendar/calendar-model.ts @@ -1,3 +1,10 @@ +/** + * CalenderEntry based on backend data type + * @property title of the event + * @property date representation of the datetime of the event + * @property time representation of the datetime of the event + * @property isCurrentDate true if datetime is today else false + */ export interface CalendarEntry { title: string date: string diff --git a/tutor-assistant-web/src/modules/calendar/components/CalendarBar.tsx b/tutor-assistant-web/src/modules/calendar/components/CalendarBar.tsx index d5c1921..1d8c5f4 100644 --- a/tutor-assistant-web/src/modules/calendar/components/CalendarBar.tsx +++ b/tutor-assistant-web/src/modules/calendar/components/CalendarBar.tsx @@ -1,33 +1,32 @@ -import { Header } from '../../../common/components/Header.tsx' -import { MainContent, Row } from '../../../lib/components/flex-layout.tsx' +import { Header } from '../../../common/components/containers/Header.tsx' +import { MainContent, Row } from '../../../common/components/containers/flex-layout.tsx' import { Button, IconButton } from '@mui/joy' import { Cached, Logout } from '@mui/icons-material' -import { Bar } from '../../../common/components/Bar.tsx' +import { Bar } from '../../../common/components/containers/Bar.tsx' import React from 'react' import { useAuth } from '../../../app/auth/useAuth.ts' import { useCalendar } from '../useCalendar.ts' import { useTranslation } from 'react-i18next' -import { Scroller } from '../../../lib/components/Scroller.tsx' -import { StyledDivider } from '../../../common/components/StyledDivider.tsx' +import { Scroller } from '../../../common/components/containers/Scroller.tsx' +import { StyledDivider } from '../../../common/components/widgets/StyledDivider.tsx' import { CalendarTable } from './CalendarTable.tsx' -interface Props { - -} - -export function CalendarBar({}: Props) { +/** + * Left sidebar containing a calendar + * Loads and manages and displays the calendar + * Additionally renders logout button at the bottom + */ +export function CalendarBar() { const { t } = useTranslation() const { logout, getRoles } = useAuth() const { calendarEntries, loadNewCalendar } = useCalendar() - console.log(calendarEntries) - const canManage = getRoles().includes('document-manager') return (
+ +