diff --git a/ui/.env.example b/ui/.env.example new file mode 100644 index 00000000..fee0fb08 --- /dev/null +++ b/ui/.env.example @@ -0,0 +1,8 @@ +VITE_GITHUB_CLIENT_ID=your_client_id +VITE_GITHUB_REDIRECT_URI=http://localhost:3000/oauth/callback +VITE_GITHUB_CLIENT_SECRET=your_client_secret +VITE_GITHUB_CLIENT_USER_IDENTITY_URL=https://github.com/login/oauth/authorize +VITE_GITHUB_LOGIN_URL=https://github.com/login/oauth/access_token +VITE_GITHUB_CORS_LOGIN_URL=https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/authorize +VITE_GITHUB_SCOPES=repo +VITE_STORAGE_EXPIRY_DURATION=21600000 diff --git a/ui/.eslintignore b/ui/.eslintignore index 6805ef16..eaa4ba38 100644 --- a/ui/.eslintignore +++ b/ui/.eslintignore @@ -3,4 +3,4 @@ *.mjs *.d.ts *.d.mts -vite.config.ts \ No newline at end of file +vite.config.ts!/coverage/ diff --git a/ui/.gitignore b/ui/.gitignore index 87c91007..f7680837 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -23,3 +23,102 @@ yarn-debug.log* yarn-error.log* /.stylelintcache /.eslintcache +!/coverage/ +!/.eslintcache +/.eslintcache +/coverage/ui/src/api/api-client.ts.html +/coverage/ui/src/App.tsx.html +/coverage/ui/src/providers/app-provider.tsx.html +/coverage/ui/src/layouts/AppLayout.tsx.html +/coverage/ui/src/providers/auth-provider/auth-provider.tsx.html +/coverage/ui/src/store/auth-store/auth-store.ts.html +/coverage/base.css +/coverage/block-navigation.js +/coverage/clover.xml +/coverage/coverage-final.json +/coverage/favicon.png +/coverage/ui/src/api/github-client.ts.html +/coverage/ui/src/components/Header/Header.tsx.html +/coverage/ui/src/api/repos/github/index.html +/coverage/ui/src/api/repos/index.html +/coverage/ui/src/api/user/github/index.html +/coverage/ui/src/api/user/index.html +/coverage/ui/src/api/index.html +/coverage/ui/src/components/Header/ReposList/index.html +/coverage/ui/src/components/Header/UserMenu/index.html +/coverage/ui/src/components/Header/index.html +/coverage/ui/src/components/Loading/index.html +/coverage/ui/src/components/NavBar/index.html +/coverage/ui/src/components/index.html +/coverage/ui/src/layouts/index.html +/coverage/ui/src/providers/auth-provider/index.html +/coverage/ui/src/providers/index.html +/coverage/ui/src/store/auth-store/index.html +/coverage/ui/src/store/index.html +/coverage/ui/src/utils/index.html +/coverage/ui/src/index.html +/coverage/ui/test-utils/index.html +/coverage/ui/index.html +/coverage/index.html +/coverage/ui/src/api/repos/github/index.ts.html +/coverage/ui/src/api/repos/index.ts.html +/coverage/ui/src/api/user/github/index.ts.html +/coverage/ui/src/api/user/index.ts.html +/coverage/ui/src/api/index.ts.html +/coverage/ui/src/components/Header/ReposList/index.ts.html +/coverage/ui/src/components/Header/UserMenu/index.ts.html +/coverage/ui/src/components/Header/index.ts.html +/coverage/ui/src/components/Loading/index.ts.html +/coverage/ui/src/components/NavBar/index.ts.html +/coverage/ui/src/components/index.ts.html +/coverage/ui/src/layouts/index.ts.html +/coverage/ui/src/providers/auth-provider/index.ts.html +/coverage/ui/src/providers/index.ts.html +/coverage/ui/src/store/auth-store/index.ts.html +/coverage/ui/src/store/index.ts.html +/coverage/ui/src/utils/index.ts.html +/coverage/ui/test-utils/index.ts.html +/coverage/ui/src/components/Loading/Loading.tsx.html +/coverage/ui/src/main.tsx.html +/coverage/ui/src/providers/mantine-provider.tsx.html +/coverage/ui/src/components/NavBar/NavBar.tsx.html +/coverage/ui/postcss.config.cjs.html +/coverage/prettify.css +/coverage/prettify.js +/coverage/ui/src/providers/react-query-provider.tsx.html +/coverage/ui/test-utils/render.tsx.html +/coverage/ui/src/api/repos/repo-query-keys.ts.html +/coverage/ui/src/components/Header/ReposList/ReposList.tsx.html +/coverage/sort-arrow-sprite.png +/coverage/sorter.js +/coverage/ui/src/theme.ts.html +/coverage/ui/src/api/repos/github/use-repos.ts.html +/coverage/ui/src/api/user/github/use-user.ts.html +/coverage/ui/src/api/user/user-query-keys.ts.html +/coverage/ui/src/components/Header/UserMenu/UserMenu.tsx.html +/coverage/ui/src/utils/yaml_to_json.ts.html +/coverage/ui/test-utils/__mocks__/dynamic-store.mock.ts.html +/coverage/ui/test-utils/__mocks__/index.html +/coverage/ui/test-utils/__mocks__/index.ts.html +/coverage/ui/test-utils/__mocks__/repo.mock.ts.html +/coverage/ui/test-utils/__mocks__/repos.mock.ts.html +/coverage/ui/test-utils/__mocks__/user.mock.ts.html +/coverage/.tmp/coverage-0.json +/coverage/.tmp/coverage-1.json +/coverage/.tmp/coverage-2.json +/coverage/.tmp/coverage-3.json +/coverage/.tmp/coverage-4.json +/coverage/.tmp/coverage-5.json +/coverage/.tmp/coverage-6.json +/coverage/.tmp/coverage-7.json +/coverage/ui/src/pages/Login/index.html +/coverage/ui/src/pages/index.html +/coverage/ui/src/routes/index.html +/coverage/ui/src/pages/Login/index.ts.html +/coverage/ui/src/pages/index.ts.html +/coverage/ui/src/routes/index.ts.html +/coverage/ui/src/utils/isAuthenticated.ts.html +/coverage/ui/src/pages/Login/Login.tsx.html +/coverage/ui/src/routes/protected.tsx.html +/coverage/ui/src/routes/router.tsx.html +/coverage/ui/src/providers/router-provider.tsx.html diff --git a/ui/.stylelintrc.json b/ui/.stylelintrc.json index 971ae9cf..5b36a97d 100644 --- a/ui/.stylelintrc.json +++ b/ui/.stylelintrc.json @@ -24,5 +24,9 @@ "ignorePseudoClasses": ["global"] } ] - } -} \ No newline at end of file + }, + "ignoreFiles": [ + "coverage/**", + "node_modules/**" + ] +} diff --git a/ui/DEVELOPER.md b/ui/DEVELOPER.md new file mode 100644 index 00000000..1c75fab0 --- /dev/null +++ b/ui/DEVELOPER.md @@ -0,0 +1,47 @@ +# Developer Setup Guide + +## Setting Up GitHub OAuth + +To set up GitHub OAuth for this application, you will need to configure several environment variables in a `.env.local` file. Below are the steps to get you started. + +1. **Create a GitHub OAuth App**: + - Go to [GitHub Developer Settings](https://github.com/settings/developers). + - Click on "New OAuth App". + - Fill in the details with your application's information: + - **Application name**: Your app's name + - **Homepage URL**: `http://localhost:3000` + - **Authorization callback URL**: `http://localhost:3000/oauth/callback` + - After creating the app, you will get a **Client ID** and **Client Secret**. + +2. **Set Environment Variables**: + - Create a file named `.env.local` in the root directory of your project. + - Copy and paste the following template into `.env.local` and fill in the values with your GitHub OAuth credentials and URLs. + + ```plaintext + VITE_GITHUB_CLIENT_ID=your_client_id + VITE_GITHUB_REDIRECT_URI=http://localhost:3000/oauth/callback + VITE_GITHUB_CLIENT_SECRET=your_client_secret + VITE_GITHUB_CLIENT_USER_IDENTITY_URL=https://github.com/login/oauth/authorize + VITE_GITHUB_LOGIN_URL=https://github.com/login/oauth/access_token + VITE_GITHUB_CORS_LOGIN_URL=https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/authorize + VITE_GITHUB_SCOPES=repo + VITE_STORAGE_EXPIRY_DURATION=21600000 + ``` + +3. **Run the Application**: + - Make sure you have all dependencies installed. + - Start the development server: + + ```sh + npm install + npm run dev + ``` + +## Additional Notes + +- The `VITE_GITHUB_CORS_LOGIN_URL` is used to handle CORS issues during development. You might need to adjust this depending on your deployment environment. +- The `VITE_STORAGE_EXPIRY_DURATION` is set to 6 hours (21600000 milliseconds). + +By following these steps, you should be able to set up and run the application with GitHub OAuth configured. + +Happy coding! diff --git a/ui/package.json b/ui/package.json index 69b4a61a..f5184d6d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,6 +7,8 @@ "build": "tsc && vite build", "preview": "vite preview", "typecheck": "tsc --noEmit", + "clean": "rm -rf node_modules && rm -rf coverage && yarn cache clean", + "ci": "npm run clean && yarn install", "code:fix": "npm run lint:fix && npm run prettier:write && npm run typecheck", "lint": "npm run lint:eslint && npm run lint:stylelint", "lint:fix": "npm run lint:eslint:fix && npm run lint:stylelint:fix", @@ -17,13 +19,15 @@ "prettier": "prettier --check \"**/*.{ts,tsx}\"", "prettier:write": "prettier --write \"**/*.{ts,tsx}\"", "vitest": "vitest run", + "vitest:cover": "vitest run --coverage", "vitest:watch": "vitest", - "test": "npm run typecheck && npm run prettier && npm run lint && npm run vitest" + "test": "npm run typecheck && npm run prettier && npm run lint && npm run vitest:cover" }, "dependencies": { "@mantine/core": "^7.9.0", "@mantine/form": "^7.9.0", "@mantine/hooks": "^7.9.0", + "@mantine/notifications": "^7.9.2", "@tabler/icons-react": "^3.3.0", "@tanstack/react-query": "^5.34.1", "@testing-library/dom": "^10.1.0", @@ -37,6 +41,7 @@ "zustand": "^4.5.2" }, "devDependencies": { + "@faker-js/faker": "^8.4.1", "@tanstack/react-query-devtools": "^5.34.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", @@ -50,6 +55,7 @@ "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^1.6.0", "eslint": "^8.57.0", "eslint-config-airbnb": "19.0.4", "eslint-config-airbnb-typescript": "^18.0.0", @@ -69,6 +75,7 @@ "stylelint-config-standard-scss": "^13.0.0", "typescript": "^5.4.5", "vite": "^5.2.10", + "vite-plugin-istanbul": "^6.0.2", "vite-tsconfig-paths": "^4.3.1", "vitest": "^1.5.2" } diff --git a/ui/src/App.tsx b/ui/src/App.tsx index a5b4d6b2..f9a93389 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,11 +1,7 @@ import '@mantine/core/styles.css'; +import '@mantine/notifications/styles.css'; import React, { FC } from 'react'; import './App.css'; -import { AppLayout } from '@/layouts'; import { AppProvider } from '@/providers'; -export const App: FC = () => ( - - - -); +export const App: FC = () => ; diff --git a/ui/src/HOC/WithLoadingAndError/WithLoadingAndError.tests.tsx b/ui/src/HOC/WithLoadingAndError/WithLoadingAndError.tests.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/ui/src/HOC/WithLoadingAndError/WithLoadingAndError.tsx b/ui/src/HOC/WithLoadingAndError/WithLoadingAndError.tsx deleted file mode 100644 index db55a22c..00000000 --- a/ui/src/HOC/WithLoadingAndError/WithLoadingAndError.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { ComponentType, FC } from 'react'; -import { Alert } from '@mantine/core'; -import { Loading } from '@/components'; - -interface WithLoadingAndErrorProps { - isLoading: boolean; - error: string | null; -} - -// TODO: Not using this, as I dont want to get into prop drilling. Keeping it for reference, -// need to investigate other venues -/* eslint-disable @typescript-eslint/no-unused-vars */ -export const withLoadingAndError = ( - WrappedComponent: ComponentType -) => { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const WithLoadingAndError: FC = ({ - isLoading, - error, - ...props - }) => { - if (isLoading) { - return ; - } - - if (error) { - return ; // You can style this or handle different types of errors differently - } - - return ; - }; -}; -/* eslint-enable @typescript-eslint/no-unused-vars */ -const ErrorComponent = () => ( - - Error! - -); diff --git a/ui/src/HOC/WithLoadingAndError/index.ts b/ui/src/HOC/WithLoadingAndError/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/ui/src/HOC/index.ts b/ui/src/HOC/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/ui/src/__tests__/theme.test.ts b/ui/src/__tests__/theme.test.ts new file mode 100644 index 00000000..60e09602 --- /dev/null +++ b/ui/src/__tests__/theme.test.ts @@ -0,0 +1,35 @@ +import { theme } from '@/theme'; + +describe('Theme Configuration', () => { + describe('Colors', () => { + it('should have deepBlue and blue color palettes defined', () => { + expect(theme.colors?.deepBlue).toBeDefined(); + expect(theme.colors?.blue).toBeDefined(); + }); + + it('should have 10 shades for deepBlue', () => { + expect(theme.colors?.deepBlue?.length).toBe(10); + }); + }); + + describe('Shadows', () => { + it('should define medium and extra-large shadows', () => { + expect(theme.shadows?.md).toBeDefined(); + expect(theme.shadows?.xl).toBeDefined(); + }); + + it('should have correct shadow value for md', () => { + expect(theme.shadows?.md).toBe('1px 1px 3px rgba(0, 0, 0, .25)'); + }); + }); + + describe('Typography', () => { + it('should have a Roboto font family for headings', () => { + expect(theme.headings?.fontFamily).toBe('Roboto, sans-serif'); + }); + + it('should define font size for h1', () => { + expect(theme.headings?.sizes?.h1?.fontSize).toBeDefined(); + }); + }); +}); diff --git a/ui/src/api/github-client.ts b/ui/src/api/github-client.ts index b3e05713..7a6eda3e 100644 --- a/ui/src/api/github-client.ts +++ b/ui/src/api/github-client.ts @@ -1,15 +1,21 @@ import axios from 'axios'; -import { useAuthStore } from '@/store'; -export const gitHubClient = () => { - //TODO: Is this a good practice? - const { token } = useAuthStore.getState(); - - return axios.create({ +export const gitHubClient = () => + axios.create({ baseURL: 'https://api.github.com/', headers: { - Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); + +const GithubLoginCredentials = { + clientId: import.meta.env.VITE_GITHUB_CLIENT_ID, + loginUrl: import.meta.env.VITE_GITHUB_CLIENT_USER_IDENTITY_URL, + redirectUri: import.meta.env.VITE_GITHUB_REDIRECT_URI, + scopes: import.meta.env.VITE_GITHUB_SCOPES, +}; + +export const githubLogin = () => { + const { loginUrl, clientId, redirectUri, scopes } = GithubLoginCredentials; + window.location.href = `${loginUrl}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scopes}`; }; diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 4f9515fb..b73abdca 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -1,4 +1,6 @@ export { apiClient } from './api-client'; +export { githubLogin } from './github-client'; export * from './user'; export * from './repos'; +export * from './oauth'; diff --git a/ui/src/api/oauth/github/index.ts b/ui/src/api/oauth/github/index.ts new file mode 100644 index 00000000..fd55eacd --- /dev/null +++ b/ui/src/api/oauth/github/index.ts @@ -0,0 +1 @@ +export * from './use-oauth'; diff --git a/ui/src/api/oauth/github/use-oauth.ts b/ui/src/api/oauth/github/use-oauth.ts new file mode 100644 index 00000000..d5e58a9c --- /dev/null +++ b/ui/src/api/oauth/github/use-oauth.ts @@ -0,0 +1,24 @@ +import axios from 'axios'; +import { useQuery } from '@tanstack/react-query'; + +export const exchangeGithubCodeForToken = async (code: string | null) => { + const data = { + client_id: import.meta.env.VITE_GITHUB_CLIENT_ID, + client_secret: import.meta.env.VITE_GITHUB_CLIENT_SECRET, + code, + redirectUri: import.meta.env.VITE_GITHUB_REDIRECT_URI, + }; + const response = await axios.post('/login/oauth/access_token', data, { + headers: { + Accept: 'application/json', + }, + }); + return response.data as OAuth; +}; + +export const useOAuth = ({ code }: { code: string | null }) => + useQuery({ + queryKey: ['Github', 'oauth', code], + queryFn: () => exchangeGithubCodeForToken(code), + retry: 1, + }); diff --git a/ui/src/api/oauth/index.ts b/ui/src/api/oauth/index.ts new file mode 100644 index 00000000..7302c7ad --- /dev/null +++ b/ui/src/api/oauth/index.ts @@ -0,0 +1 @@ +export * from './github'; diff --git a/ui/src/api/oauth/oauth.d.ts b/ui/src/api/oauth/oauth.d.ts new file mode 100644 index 00000000..a865ade4 --- /dev/null +++ b/ui/src/api/oauth/oauth.d.ts @@ -0,0 +1,5 @@ +type OAuth = { + access_token: string; + scope: string; + token_type: string; +}; diff --git a/ui/src/api/repos/github/use-repos.ts b/ui/src/api/repos/github/use-repos.ts index ef00cb49..0ecd83cb 100644 --- a/ui/src/api/repos/github/use-repos.ts +++ b/ui/src/api/repos/github/use-repos.ts @@ -6,12 +6,15 @@ import { repoQueryKeys } from '../repo-query-keys'; export const getReposFn = async () => { const { username } = useAuthStore.getState(); - const response = await apiClient.get(`/users/${username || ''}/repos`); + const response = await apiClient.get(`/users/${username || ''}/repos`, { + headers: { + Authorization: `Bearer ${useAuthStore.getState().token}`, + }, + }); return response.data as Repos[]; }; -// TODO: How to convert Github User to generic Repo type? export const useRepos = () => useQuery({ queryKey: repoQueryKeys.details(), diff --git a/ui/src/api/repos/index.ts b/ui/src/api/repos/index.ts index 7302c7ad..da45b74c 100644 --- a/ui/src/api/repos/index.ts +++ b/ui/src/api/repos/index.ts @@ -1 +1 @@ -export * from './github'; +export { useRepos } from './github'; diff --git a/ui/src/api/user/github/user.d.ts b/ui/src/api/user/github/types.ts similarity index 91% rename from ui/src/api/user/github/user.d.ts rename to ui/src/api/user/github/types.ts index 6f4e5fbb..e8a817f8 100644 --- a/ui/src/api/user/github/user.d.ts +++ b/ui/src/api/user/github/types.ts @@ -1,4 +1,5 @@ -interface User { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +interface GitHubUser { login: string; id: number; node_id: string; diff --git a/ui/src/api/user/github/use-user.ts b/ui/src/api/user/github/use-user.ts index b8c9fd9e..226f44ee 100644 --- a/ui/src/api/user/github/use-user.ts +++ b/ui/src/api/user/github/use-user.ts @@ -1,17 +1,20 @@ import { useQuery } from '@tanstack/react-query'; import { apiClient } from '@/api'; import { userQueryKeys } from '../user-query-keys'; +import { useAuthStore } from '@/store'; export const getUserFn = async () => { - const response = await apiClient.get('/user'); + const response = await apiClient.get('/user', { + headers: { + Authorization: `Bearer ${useAuthStore.getState().token}`, + }, + }); return response.data as User; }; -// TODO: How to convert Github User to generic User type? export const useUser = () => useQuery({ queryKey: userQueryKeys.detail('me'), queryFn: getUserFn, - retry: 4, }); diff --git a/ui/src/api/user/user.d.ts b/ui/src/api/user/user.d.ts new file mode 100644 index 00000000..506ece8d --- /dev/null +++ b/ui/src/api/user/user.d.ts @@ -0,0 +1,7 @@ +// Here is the type that will be User across the codebase, +// If integrating with a different git provider, +// you will need to update the type of the user object. +type User = Pick< + GitHubUser, + 'avatar_url' | 'name' | 'login' | 'id' | 'organizations_url' | 'subscriptions_url' +>; diff --git a/ui/src/components/Header/ReposList/ReposList.test.tsx b/ui/src/components/Header/ReposList/ReposList.test.tsx deleted file mode 100644 index 6b689049..00000000 --- a/ui/src/components/Header/ReposList/ReposList.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen } from '@test-utils'; -import { ReposList } from './ReposList'; -import { useRepos } from '@/api'; - -// Mock the API hook -vi.mock('@/api', () => ({ - useRepos: vi.fn(), -})); - -describe('ReposList', () => { - it('renders loading state correctly', () => { - // @ts-ignore - useRepos.mockReturnValue({ data: null, error: null, isLoading: true }); - render(); - expect(screen.getByTestId('loading-user-menu')).toBeInTheDocument(); - }); - - it('renders error state correctly', () => { - // @ts-ignore - useRepos.mockReturnValue({ data: null, error: 'Failed to fetch', isLoading: false }); - render(); - expect(screen.getByText('Error!')).toBeInTheDocument(); - }); - - it('renders repository list correctly', () => { - const repos = [{ name: 'Repo1' }, { name: 'Repo2' }, { name: 'Repo3' }]; - // @ts-ignore - useRepos.mockReturnValue({ data: repos, error: null, isLoading: false }); - render(); - expect(screen.getByText('Repo1')).toBeInTheDocument(); - expect(screen.getByText('Repo2')).toBeInTheDocument(); - expect(screen.getByText('Repo3')).toBeInTheDocument(); - }); -}); diff --git a/ui/src/components/Header/ReposList/ReposList.tsx b/ui/src/components/Header/ReposList/ReposList.tsx deleted file mode 100644 index 5d5402f6..00000000 --- a/ui/src/components/Header/ReposList/ReposList.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { - Alert, - Combobox, - ComboboxDropdown, - ComboboxEmpty, - ComboboxOption, - ComboboxOptions, - ComboboxTarget, - ScrollAreaAutosize, - Skeleton, - TextInput, - useCombobox, -} from '@mantine/core'; -import { useState } from 'react'; -import { useRepos } from '@/api'; - -export const ReposList = () => { - const combobox = useCombobox({ - scrollBehavior: 'smooth', - onDropdownClose: () => combobox.resetSelectedOption(), - }); - - const { data, error, isLoading } = useRepos(); - - const [value, setValue] = useState(''); - - if (error) { - return ; - } - - if (isLoading) { - return ; - } - - const shouldFilterOptions = !data?.some((item) => item.name === value); - const filteredOptions = shouldFilterOptions - ? data?.filter((item) => item.name.toLowerCase().includes(value.toLowerCase().trim())) - : data; - - const options = filteredOptions?.map((item: any) => ( - - {item.name} - - )); - - return ( - { - setValue(val); - combobox.closeDropdown(); - }} - > - - { - setValue(e.currentTarget.value); - combobox.openDropdown(); - combobox.updateSelectedOptionIndex(); - }} - onClick={() => combobox.openDropdown()} - onFocus={() => combobox.openDropdown()} - onBlur={() => combobox.closeDropdown()} - /> - - - - - - {options?.length === 0 ? Nothing Found : options} - - - - - ); -}; - -const LoadingSkeleton = () => ( -
- - - -
-); - -const ErrorComponent = () => ( - - Error! - -); diff --git a/ui/src/components/Header/ReposList/index.ts b/ui/src/components/Header/ReposList/index.ts deleted file mode 100644 index 827033e5..00000000 --- a/ui/src/components/Header/ReposList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ReposList } from './ReposList'; diff --git a/ui/src/components/Loading/Loading.tests.tsx b/ui/src/components/Loading/Loading.test.tsx similarity index 100% rename from ui/src/components/Loading/Loading.tests.tsx rename to ui/src/components/Loading/Loading.test.tsx diff --git a/ui/src/components/Loading/Loading.tsx b/ui/src/components/Loading/Loading.tsx index 4445f0f4..23a1ec77 100644 --- a/ui/src/components/Loading/Loading.tsx +++ b/ui/src/components/Loading/Loading.tsx @@ -5,7 +5,6 @@ interface LoadingProps { message?: string; // Optional prop to display a message below the spinner } -// TODO: Look into handling loaders via state. export const Loading: FC = ({ message = 'Loading...' }) => (
diff --git a/ui/src/components/NavBar/NavBar.tests.ts b/ui/src/components/NavBar/NavBar.tests.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/ui/src/components/NavBar/NavBar.tsx b/ui/src/components/NavBar/NavBar.tsx deleted file mode 100644 index be8f8b9a..00000000 --- a/ui/src/components/NavBar/NavBar.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import './NavBar.css'; -import { ReposList } from '@/components/Header/ReposList'; - -export const NavBar = () => ( - -); diff --git a/ui/src/components/OAuthCallback/OAuthCallback.test.tsx b/ui/src/components/OAuthCallback/OAuthCallback.test.tsx new file mode 100644 index 00000000..7d63d76c --- /dev/null +++ b/ui/src/components/OAuthCallback/OAuthCallback.test.tsx @@ -0,0 +1,57 @@ +import { describe } from 'vitest'; +import { render, screen } from '@test-utils'; +import { waitFor } from '@testing-library/react'; +import { useNavigate } from 'react-router-dom'; +import { useOAuth } from '@/api'; +import { OAuthCallback } from '@/components'; +import { useAuthStore } from '@/store'; + +describe('OAuthCallback', () => { + test('displays loading message while fetching data', () => { + //@ts-ignore + useOAuth.mockReturnValue({ data: null, isLoading: true, isError: false }); + render(); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + test('sets token and navigates upon receving data', async () => { + const mockSetToken = vi.fn(); + const mockNavigate = vi.fn(); + //@ts-ignore + useOAuth.mockReturnValue({ + data: { access_token: 'token' }, + isLoading: false, + isError: false, + }); + // @ts-ignore + useAuthStore.mockReturnValue({ setToken: mockSetToken }); + // @ts-ignore + useNavigate.mockReturnValue(mockNavigate); + + render(); + await waitFor( + () => { + expect(mockSetToken).toHaveBeenCalledWith('token'); + expect(mockNavigate).toHaveBeenCalledWith('/'); + }, + { + timeout: 1000, + } + ); + }); + + test('displays error message if error occurs', () => { + const mockNavigate = vi.fn(); + //@ts-ignore + useOAuth.mockReturnValue({ + data: null, + isLoading: false, + isError: true, + error: { message: 'error' }, + }); + //@ts-ignore + useNavigate.mockReturnValue(mockNavigate); + render(); + expect(mockNavigate).toHaveBeenCalledWith('/login'); + }); +}); diff --git a/ui/src/components/OAuthCallback/OAuthCallback.tsx b/ui/src/components/OAuthCallback/OAuthCallback.tsx new file mode 100644 index 00000000..2baff539 --- /dev/null +++ b/ui/src/components/OAuthCallback/OAuthCallback.tsx @@ -0,0 +1,34 @@ +import { FC, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { notifications } from '@mantine/notifications'; +import { useOAuth } from '@/api'; +import { useAuthStore } from '@/store'; +import { Loading } from '@/components'; + +export const OAuthCallback: FC = () => { + const code = new URLSearchParams(window.location.search).get('code'); + const { data, isLoading, isError, error } = useOAuth({ code }); + const navigate = useNavigate(); + const { setToken } = useAuthStore(); + + useEffect(() => { + if (data) { + setToken(data.access_token); + navigate('/'); + } + }, [data, setToken, navigate]); + + if (isLoading) { + return ; + } + + if (isError) { + notifications.show({ + title: 'Error while authenticating, redirecting to login page', + message: error.message, + }); + navigate('/login'); + } + + return null; +}; diff --git a/ui/src/components/OAuthCallback/index.ts b/ui/src/components/OAuthCallback/index.ts new file mode 100644 index 00000000..2b614843 --- /dev/null +++ b/ui/src/components/OAuthCallback/index.ts @@ -0,0 +1 @@ +export { OAuthCallback } from './OAuthCallback'; diff --git a/ui/src/components/Header/UserMenu/UserMenu.css b/ui/src/components/UserMenu/UserMenu.css similarity index 100% rename from ui/src/components/Header/UserMenu/UserMenu.css rename to ui/src/components/UserMenu/UserMenu.css diff --git a/ui/src/components/Header/UserMenu/UserMenu.test.tsx b/ui/src/components/UserMenu/UserMenu.test.tsx similarity index 89% rename from ui/src/components/Header/UserMenu/UserMenu.test.tsx rename to ui/src/components/UserMenu/UserMenu.test.tsx index 0bdb1480..8f66d86e 100644 --- a/ui/src/components/Header/UserMenu/UserMenu.test.tsx +++ b/ui/src/components/UserMenu/UserMenu.test.tsx @@ -1,14 +1,9 @@ // -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { render, screen } from '@test-utils'; import { UserMenu } from './UserMenu'; import { useUser } from '@/api'; -// Mock the API hook and store -vi.mock('@/api', () => ({ - useUser: vi.fn(), -})); - describe('UserMenu', () => { it('renders loading state correctly', () => { // @ts-ignore diff --git a/ui/src/components/Header/UserMenu/UserMenu.tsx b/ui/src/components/UserMenu/UserMenu.tsx similarity index 93% rename from ui/src/components/Header/UserMenu/UserMenu.tsx rename to ui/src/components/UserMenu/UserMenu.tsx index e4e0056b..c1471cb3 100644 --- a/ui/src/components/Header/UserMenu/UserMenu.tsx +++ b/ui/src/components/UserMenu/UserMenu.tsx @@ -3,14 +3,10 @@ import { IconAlertCircle, IconChevronDown, IconSettings } from '@tabler/icons-re import './UserMenu.css'; import { useState } from 'react'; import { useUser } from '@/api'; -import { useAuthStore } from '@/store'; export const UserMenu = () => { const { data, error, isLoading } = useUser(); const [userMenuOpened, setUserMenuOpened] = useState(false); - const { openModal } = useAuthStore((state) => ({ - openModal: state.openModal, - })); if (isLoading) { return ; @@ -44,7 +40,6 @@ export const UserMenu = () => { Settings } - onClick={openModal} > Change Token diff --git a/ui/src/components/Header/UserMenu/index.ts b/ui/src/components/UserMenu/index.ts similarity index 100% rename from ui/src/components/Header/UserMenu/index.ts rename to ui/src/components/UserMenu/index.ts diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 89c715e9..f4a1ee49 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -1,2 +1,4 @@ -export { Header } from './Header'; +export { Header } from '../layouts/Header'; export { Loading } from './Loading'; +export { OAuthCallback } from './OAuthCallback'; +export { UserMenu } from './UserMenu'; diff --git a/ui/src/layouts/AppLayout.tsx b/ui/src/layouts/AppLayout.tsx index a89b0dc2..9d0aef93 100644 --- a/ui/src/layouts/AppLayout.tsx +++ b/ui/src/layouts/AppLayout.tsx @@ -1,25 +1,15 @@ import { AppShell } from '@mantine/core'; import React from 'react'; -import { Header } from '@/components'; -import { NavBar } from '@/components/NavBar'; +import { NavBar } from './NavBar'; +import { Header } from './Header'; export const AppLayout = () => ( - {/*TODO: data-testid is not retained after the component is mounted. Need to investigate this*/}
- - {/**/} - {/* Navbar*/} - {/* {Array(15)*/} - {/* .fill(0)*/} - {/* .map((_, index) => (*/} - {/* */} - {/* ))}*/} - {/**/} Main ); diff --git a/ui/src/components/Header/Header.css b/ui/src/layouts/Header/Header.css similarity index 100% rename from ui/src/components/Header/Header.css rename to ui/src/layouts/Header/Header.css diff --git a/ui/src/components/Header/Header.test.tsx b/ui/src/layouts/Header/Header.test.tsx similarity index 100% rename from ui/src/components/Header/Header.test.tsx rename to ui/src/layouts/Header/Header.test.tsx diff --git a/ui/src/components/Header/Header.tsx b/ui/src/layouts/Header/Header.tsx similarity index 89% rename from ui/src/components/Header/Header.tsx rename to ui/src/layouts/Header/Header.tsx index 8e259dbb..e5b24201 100644 --- a/ui/src/components/Header/Header.tsx +++ b/ui/src/layouts/Header/Header.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import { Container, Group } from '@mantine/core'; -import { UserMenu } from './UserMenu'; +import { UserMenu } from '@/components'; import './Header.css'; export const Header: FC = () => ( diff --git a/ui/src/components/Header/index.ts b/ui/src/layouts/Header/index.ts similarity index 100% rename from ui/src/components/Header/index.ts rename to ui/src/layouts/Header/index.ts diff --git a/ui/src/components/NavBar/NavBar.css b/ui/src/layouts/NavBar/NavBar.css similarity index 100% rename from ui/src/components/NavBar/NavBar.css rename to ui/src/layouts/NavBar/NavBar.css diff --git a/ui/src/layouts/NavBar/NavBar.tsx b/ui/src/layouts/NavBar/NavBar.tsx new file mode 100644 index 00000000..a7a977a8 --- /dev/null +++ b/ui/src/layouts/NavBar/NavBar.tsx @@ -0,0 +1,3 @@ +import './NavBar.css'; + +export const NavBar = () => ; diff --git a/ui/src/components/NavBar/index.ts b/ui/src/layouts/NavBar/index.ts similarity index 100% rename from ui/src/components/NavBar/index.ts rename to ui/src/layouts/NavBar/index.ts diff --git a/ui/src/pages/Login/Login.css b/ui/src/pages/Login/Login.css new file mode 100644 index 00000000..4b162552 --- /dev/null +++ b/ui/src/pages/Login/Login.css @@ -0,0 +1,11 @@ +.login-page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +.login-title { + margin-bottom: 20px; +} diff --git a/ui/src/pages/Login/Login.test.tsx b/ui/src/pages/Login/Login.test.tsx new file mode 100644 index 00000000..e09dcb31 --- /dev/null +++ b/ui/src/pages/Login/Login.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, screen, waitFor, userEvent } from '@test-utils'; +import { vi } from 'vitest'; +import { Login } from '@/pages'; +import { githubLogin } from '@/api'; + +describe('Login Component', () => { + beforeEach(() => { + // Clear all mocks before each test + vi.clearAllMocks(); + }); + + it('renders correctly', () => { + render(); + expect(screen.getByText('Spark Expectations')).toBeInTheDocument(); + expect( + screen.getByText('Please login using one of the following providers:') + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Login with GitHub' })).toBeInTheDocument(); + }); + + it('calls githubLogin on button click', async () => { + render(); + const loginButton = screen.getByRole('button', { name: 'Login with GitHub' }); + userEvent.click(loginButton); + await waitFor(() => expect(githubLogin).toHaveBeenCalled()); + }); + + it('handles the login process correctly', async () => { + // Assume githubLogin is a promise that resolves to an access token + (githubLogin as jest.Mock).mockResolvedValue({ access_token: 'fake-token' }); + + render(); + const loginButton = screen.getByRole('button', { name: 'Login with GitHub' }); + userEvent.click(loginButton); + + await waitFor(() => { + // You can add additional assertions here to check for changes in the UI or redirects + expect(githubLogin).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/src/pages/Login/Login.tsx b/ui/src/pages/Login/Login.tsx new file mode 100644 index 00000000..92cfaee6 --- /dev/null +++ b/ui/src/pages/Login/Login.tsx @@ -0,0 +1,26 @@ +import { FC } from 'react'; +import { Button, Container, Paper, Text, Title } from '@mantine/core'; + +import { githubLogin } from '@/api'; + +export const Login: FC = () => { + const loginWithGithub = () => { + githubLogin(); + }; + return ( + <> + + Spark Expectations + + Please login using one of the following providers: + + + + + + + + ); +}; diff --git a/ui/src/pages/Login/index.ts b/ui/src/pages/Login/index.ts new file mode 100644 index 00000000..a10c3a83 --- /dev/null +++ b/ui/src/pages/Login/index.ts @@ -0,0 +1 @@ +export * from './Login'; diff --git a/ui/src/pages/index.ts b/ui/src/pages/index.ts new file mode 100644 index 00000000..a10c3a83 --- /dev/null +++ b/ui/src/pages/index.ts @@ -0,0 +1 @@ +export * from './Login'; diff --git a/ui/src/providers/app-provider.tsx b/ui/src/providers/app-provider.tsx index 1610ae6b..82e5975f 100644 --- a/ui/src/providers/app-provider.tsx +++ b/ui/src/providers/app-provider.tsx @@ -1,12 +1,14 @@ import React from 'react'; +import { Notifications } from '@mantine/notifications'; import { ReactQueryProvider } from './react-query-provider'; import { CustomMantineProvider } from './mantine-provider'; -import { AuthProvider } from './auth-provider'; +import { RouterProvider } from './router-provider'; -export const AppProvider = ({ children }: { children: React.ReactNode }) => ( +export const AppProvider = () => ( - {children} + + ); diff --git a/ui/src/providers/auth-provider/auth-provider.test.tsx b/ui/src/providers/auth-provider/auth-provider.test.tsx deleted file mode 100644 index 2475d675..00000000 --- a/ui/src/providers/auth-provider/auth-provider.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { AppProvider } from '@/providers'; -import { useAuthStore } from '@/store'; - -const TestConsumer = () => { - const { token, setToken } = useAuthStore(); - return ( -
- Token: {token} - -
- ); -}; - -describe('AuthProvider', () => { - it('provides the auth context correctly', () => { - render( - - - - ); - expect(screen.getByText('Token')).toBeInTheDocument(); - }); -}); diff --git a/ui/src/providers/auth-provider/auth-provider.tsx b/ui/src/providers/auth-provider/auth-provider.tsx deleted file mode 100644 index 94c8abe5..00000000 --- a/ui/src/providers/auth-provider/auth-provider.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { ReactNode, useEffect } from 'react'; -import { Modal, TextInput, Button, Group } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useAuthStore } from '@/store'; -import { getUserFn } from '@/api'; -import { Loading } from '@/components/Loading/Loading'; - -interface AuthProviderProps { - children: ReactNode; -} - -export const AuthProvider: React.FC = ({ children }) => { - const { token, username, isModalOpen, setUserName, setToken, openModal, closeModal } = - useAuthStore(); - - const form = useForm({ - initialValues: { - token, - }, - validate: { - token: (value: string | null) => value && value.length === 0 && 'Token is required', - }, - }); - - useEffect(() => { - if (!token) { - openModal(); - } else { - closeModal(); - } - }, [token, openModal, closeModal]); - - const handleLogin = (values: { token: null | string }) => { - setToken(values?.token); - - getUserFn().then((res) => { - setUserName(res.login); - }); - - closeModal(); - }; - - const handleCancel = () => { - if (token) closeModal(); - }; - - return ( - <> - -
handleLogin(values))}> - - - - - - -
- {/*TODO: Handle Error State*/} - {token && username ? children : } - - ); -}; diff --git a/ui/src/providers/auth-provider/index.ts b/ui/src/providers/auth-provider/index.ts deleted file mode 100644 index 83ad66de..00000000 --- a/ui/src/providers/auth-provider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AuthProvider } from './auth-provider'; diff --git a/ui/src/providers/index.ts b/ui/src/providers/index.ts index f01e5ba2..37ac7157 100644 --- a/ui/src/providers/index.ts +++ b/ui/src/providers/index.ts @@ -1 +1,4 @@ export { AppProvider } from './app-provider'; +export { CustomMantineProvider } from './mantine-provider'; +export { RouterProvider } from './router-provider'; +export { ReactQueryProvider } from './react-query-provider'; diff --git a/ui/src/providers/mantine-provider.tsx b/ui/src/providers/mantine-provider.tsx index 98aceae6..1ea3fcbb 100644 --- a/ui/src/providers/mantine-provider.tsx +++ b/ui/src/providers/mantine-provider.tsx @@ -1,7 +1,7 @@ -import { createTheme, MantineProvider } from '@mantine/core'; +import { MantineProvider } from '@mantine/core'; import React from 'react'; import { theme } from '@/theme'; export const CustomMantineProvider = ({ children }: { children: React.ReactNode }) => ( - {children} + {children} ); diff --git a/ui/src/providers/react-query-provider.tsx b/ui/src/providers/react-query-provider.tsx index 35272306..8bcd8aca 100644 --- a/ui/src/providers/react-query-provider.tsx +++ b/ui/src/providers/react-query-provider.tsx @@ -1,8 +1,23 @@ import { PropsWithChildren } from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { notifications } from '@mantine/notifications'; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + }, + }, + queryCache: new QueryCache({ + onError: (error, query) => + notifications.show({ + title: `${error.name} for ${query.queryKey}`, + message: error.message, + color: 'red', + }), + }), +}); export const ReactQueryProvider = ({ children }: PropsWithChildren) => ( diff --git a/ui/src/providers/router-provider.tsx b/ui/src/providers/router-provider.tsx new file mode 100644 index 00000000..b0bfaf51 --- /dev/null +++ b/ui/src/providers/router-provider.tsx @@ -0,0 +1,4 @@ +import { RouterProvider as ReactRouterProvider } from 'react-router-dom'; +import { router } from '@/routes'; + +export const RouterProvider = () => ; diff --git a/ui/src/routes/index.ts b/ui/src/routes/index.ts new file mode 100644 index 00000000..164ab508 --- /dev/null +++ b/ui/src/routes/index.ts @@ -0,0 +1 @@ +export * from './router'; diff --git a/ui/src/routes/protected.tsx b/ui/src/routes/protected.tsx new file mode 100644 index 00000000..4778e5cd --- /dev/null +++ b/ui/src/routes/protected.tsx @@ -0,0 +1,12 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { useAuthStore } from '@/store'; + +export const Protected = () => { + const { token } = useAuthStore.getState(); + + if (!token) { + return ; + } + + return ; +}; diff --git a/ui/src/routes/router.tsx b/ui/src/routes/router.tsx new file mode 100644 index 00000000..75069b85 --- /dev/null +++ b/ui/src/routes/router.tsx @@ -0,0 +1,19 @@ +import { createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom'; +import { Protected } from '@/routes/protected'; +import { AppLayout } from '@/layouts'; +import { Login } from '@/pages'; +import { OAuthCallback } from '@/components'; + +export const router = createBrowserRouter( + createRoutesFromElements( + + }> + } /> + + {/*} />*/} + } /> + } /> + Not Found} /> + + ) +); diff --git a/ui/src/store/auth-store/auth-store.test.ts b/ui/src/store/auth-store/auth-store.test.ts index 196aeda3..24c53c4b 100644 --- a/ui/src/store/auth-store/auth-store.test.ts +++ b/ui/src/store/auth-store/auth-store.test.ts @@ -1,24 +1,9 @@ // tests for src/store/auth-store.ts -import { act } from 'react-dom/test-utils'; +import { act } from 'react'; import { renderHook } from '@testing-library/react'; import { useAuthStore } from '@/store/auth-store'; describe('useAuthStore', () => { - it('should toggle modal open and close', () => { - const { result } = renderHook(() => useAuthStore()); - expect(result.current.isModalOpen).toBe(false); - - act(() => { - result.current.openModal(); - }); - expect(result.current.isModalOpen).toBe(true); - - act(() => { - result.current.closeModal(); - }); - expect(result.current.isModalOpen).toBe(false); - }); - it('should set and update token', () => { const { result } = renderHook(() => useAuthStore()); expect(result.current.token).toBeNull(); diff --git a/ui/src/store/auth-store/auth-store.ts b/ui/src/store/auth-store/auth-store.ts index 0229fd9e..fbee72a8 100644 --- a/ui/src/store/auth-store/auth-store.ts +++ b/ui/src/store/auth-store/auth-store.ts @@ -1,47 +1,32 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; +import { customStorage } from '../custom-store'; interface AuthState { token: string | null; - isModalOpen: boolean; username: string | null; setToken: (token: string | null) => void; setUserName: (username: string) => void; - openModal: () => void; - closeModal: () => void; } -/* - * Coupling modals with state management. - * Not sure if it's a good idea to do this. - * - * Keep an eye on this approach - * Potential TODO - * */ - export const useAuthStore = create( persist( (set: any) => ({ token: null, - isModalOpen: false, username: null, setToken: (token: string | null) => set(() => ({ token })), setUserName: (username: string) => set(() => ({ username })), - openModal: () => set(() => ({ isModalOpen: true })), - closeModal: () => set(() => ({ isModalOpen: false })), }), { name: 'auth', + storage: customStorage, } ) ); - +// // // export const useAuthStore = create((set) => ({ // token: null, -// isModalOpen: false, // username: null, -// setToken: (token: string) => set(() => ({ token })), +// setToken: (token: string | null) => set(() => ({ token })), // setUserName: (username: string) => set(() => ({ username })), -// openModal: () => set(() => ({ isModalOpen: true })), -// closeModal: () => set(() => ({ isModalOpen: false })), // })); diff --git a/ui/src/store/custom-store.ts b/ui/src/store/custom-store.ts new file mode 100644 index 00000000..deee999c --- /dev/null +++ b/ui/src/store/custom-store.ts @@ -0,0 +1,29 @@ +import { PersistStorage } from 'zustand/middleware'; + +export const customStorage: PersistStorage = { + getItem: (name) => { + const item = localStorage.getItem(name); + if (!item) return null; + + const parsedItem = JSON.parse(item); + const now = new Date().getTime(); + + if (parsedItem.expiry && now > parsedItem.expiry) { + localStorage.removeItem(name); + return null; + } + + return parsedItem.value; + }, + setItem: (name, value) => { + const now = new Date().getTime(); + const item = { + value, + expiry: now + 6 * 60 * 60 * 1000, // 6 hours from now + }; + localStorage.setItem(name, JSON.stringify(item)); + }, + removeItem: (name) => { + localStorage.removeItem(name); + }, +}; diff --git a/ui/src/theme.ts b/ui/src/theme.ts index 9868cfb8..fbe6de71 100644 --- a/ui/src/theme.ts +++ b/ui/src/theme.ts @@ -1,6 +1,43 @@ -import { createTheme } from '@mantine/core'; +import { createTheme, rem } from '@mantine/core'; -// TODO: LOOK INTO LIGHT DARK THEME export const theme = createTheme({ - /** Put your mantine theme override here */ + colors: { + deepBlue: [ + '#eef3ff', + '#dce4f5', + '#b9c7e2', + '#94a8d0', + '#748dc1', + '#5f7cb8', + '#5474b4', + '#44639f', + '#39588f', + '#2d4b81', + ], + + blue: [ + '#eef3ff', + '#dee2f2', + '#bdc2de', + '#98a0ca', + '#7a84ba', + '#6672b0', + '#5c68ac', + '#4c5897', + '#424e88', + '#364379', + ], + }, + + shadows: { + md: '1px 1px 3px rgba(0, 0, 0, .25)', + xl: '5px 5px 3px rgba(0, 0, 0, .25)', + }, + + headings: { + fontFamily: 'Roboto, sans-serif', + sizes: { + h1: { fontSize: rem(36) }, + }, + }, }); diff --git a/ui/test-utils/__mocks__/index.ts b/ui/test-utils/__mocks__/index.ts new file mode 100644 index 00000000..41db9682 --- /dev/null +++ b/ui/test-utils/__mocks__/index.ts @@ -0,0 +1,3 @@ +export * from './user.mock'; +export * from './repo.mock'; +export * from './repos.mock'; diff --git a/ui/test-utils/__mocks__/repo.mock.ts b/ui/test-utils/__mocks__/repo.mock.ts new file mode 100644 index 00000000..dc4ff8e1 --- /dev/null +++ b/ui/test-utils/__mocks__/repo.mock.ts @@ -0,0 +1,56 @@ +import { faker } from '@faker-js/faker'; + +const createSecurityStatusMock = (): SecurityStatus => ({ + status: faker.helpers.arrayElement(['enabled', 'disabled']), +}); + +const createSecurityAndAnalysisMock = (): SecurityAndAnalysis => ({ + advanced_security: createSecurityStatusMock(), + secret_scanning: createSecurityStatusMock(), + secret_scanning_push_protection: createSecurityStatusMock(), +}); + +const createPermissionsMock = (): Permissions => + { + admin: faker.datatype.boolean(), + push: faker.datatype.boolean(), + pull: faker.datatype.boolean(), + }; + +const createOwnerMock = (): Owner => ({ + login: faker.internet.userName(), + id: faker.number.int(), + node_id: faker.string.uuid(), + avatar_url: faker.image.avatar(), + gravatar_id: '', + url: faker.internet.url(), + html_url: faker.internet.url(), + followers_url: faker.internet.url(), + following_url: faker.internet.url(), + gists_url: faker.internet.url(), + starred_url: faker.internet.url(), + subscriptions_url: faker.internet.url(), + organizations_url: faker.internet.url(), + repos_url: faker.internet.url(), + events_url: faker.internet.url(), + received_events_url: faker.internet.url(), + type: 'User', + site_admin: faker.datatype.boolean(), +}); + +export const createRepoMock = (): Repos => + { + id: faker.number.int(), + node_id: faker.string.uuid(), + name: faker.company.name(), + full_name: `${faker.company.name()}/${faker.internet.userName()}`, + owner: createOwnerMock(), + private: faker.datatype.boolean(), + html_url: faker.internet.url(), + description: faker.lorem.sentence(), + fork: faker.datatype.boolean(), + url: faker.internet.url(), + permissions: createPermissionsMock(), + security_and_analysis: createSecurityAndAnalysisMock(), + // Populate other URLs and properties as needed... + }; diff --git a/ui/test-utils/__mocks__/repos.mock.ts b/ui/test-utils/__mocks__/repos.mock.ts new file mode 100644 index 00000000..036f229f --- /dev/null +++ b/ui/test-utils/__mocks__/repos.mock.ts @@ -0,0 +1,7 @@ +import { createRepoMock } from './repo.mock'; + +export const useReposMock = () => ({ + data: Array.from({ length: 10 }, createRepoMock), + isLoading: false, + isError: false, +}); diff --git a/ui/test-utils/__mocks__/user.mock.ts b/ui/test-utils/__mocks__/user.mock.ts new file mode 100644 index 00000000..8b905c55 --- /dev/null +++ b/ui/test-utils/__mocks__/user.mock.ts @@ -0,0 +1,66 @@ +import { faker } from '@faker-js/faker'; + +export const createPlanMock = (): Plan => ({ + name: faker.company.name(), + space: faker.number.int({ min: 1000, max: 10000 }), + private_repos: faker.number.int({ max: 100 }), + collaborators: faker.number.int({ max: 10 }), +}); + +export const createUserMock = (): User => ({ + login: faker.internet.userName(), + id: faker.number.int(), + avatar_url: faker.image.avatar(), + subscriptions_url: faker.internet.url(), + organizations_url: faker.internet.url(), + name: faker.person.fullName(), +}); + +// +// export const createUserMock = (): User => ({ +// login: faker.internet.userName(), +// id: faker.number.int(), +// node_id: faker.string.uuid(), +// avatar_url: faker.image.avatar(), +// gravatar_id: '', +// url: faker.internet.url(), +// html_url: faker.internet.url(), +// followers_url: faker.internet.url(), +// following_url: faker.internet.url(), +// gists_url: faker.internet.url(), +// starred_url: faker.internet.url(), +// subscriptions_url: faker.internet.url(), +// organizations_url: faker.internet.url(), +// repos_url: faker.internet.url(), +// events_url: faker.internet.url(), +// received_events_url: faker.internet.url(), +// type: 'User', +// site_admin: faker.datatype.boolean(), +// name: faker.person.fullName(), +// company: faker.company.name(), +// blog: faker.internet.url(), +// location: faker.location.secondaryAddress(), +// email: faker.internet.email(), +// hireable: faker.datatype.boolean(), +// bio: faker.lorem.sentence(), +// twitter_username: faker.internet.userName(), +// public_repos: faker.number.int({ max: 100 }), +// public_gists: faker.number.int({ max: 100 }), +// followers: faker.number.int({ max: 1000 }), +// following: faker.number.int({ max: 1000 }), +// created_at: faker.date.past().toISOString(), +// updated_at: faker.date.recent().toISOString(), +// private_gists: faker.number.int({ max: 100 }), +// total_private_repos: faker.number.int({ max: 100 }), +// owned_private_repos: faker.number.int({ max: 100 }), +// disk_usage: faker.number.int({ max: 10000 }), +// collaborators: faker.number.int({ max: 10 }), +// two_factor_authentication: faker.datatype.boolean(), +// plan: createPlanMock(), +// }); + +export const useUserMock = () => ({ + data: createUserMock(), + isLoading: false, + isError: false, +}); diff --git a/ui/test-utils/index.ts b/ui/test-utils/index.ts index 15d3a0e4..c1da34b0 100644 --- a/ui/test-utils/index.ts +++ b/ui/test-utils/index.ts @@ -1,5 +1,5 @@ import userEvent from '@testing-library/user-event'; +import { screen, waitFor } from '@testing-library/react'; -export * from '@testing-library/react'; -export { render } from './render'; -export { userEvent }; +export { render, renderWithOutMocks } from './render'; +export { userEvent, screen, waitFor }; diff --git a/ui/test-utils/render.tsx b/ui/test-utils/render.tsx index 87f49c3e..b5d7f46e 100644 --- a/ui/test-utils/render.tsx +++ b/ui/test-utils/render.tsx @@ -1,27 +1,88 @@ import { render as testingLibraryRender } from '@testing-library/react'; import React from 'react'; import { vi } from 'vitest'; -import { AppProvider } from '@/providers'; +import { MemoryRouter } from 'react-router-dom'; +import { createUserMock, useReposMock, useUserMock } from './__mocks__'; +import { CustomMantineProvider, ReactQueryProvider } from '@/providers'; export function render(ui: React.ReactNode) { - vi.mock('@/store', () => ({ - useAuthStore: vi.fn(() => ({ - token: 'mock-token', - username: 'mock-username', - openModal: vi.fn(), - closeModal: vi.fn(), - })), - })); - - vi.mock('@/api/github-client', () => ({ - gitHubClient: vi.fn(() => ({ - get: vi.fn(() => Promise.resolve({ data: 'mocked data' })), - post: vi.fn(() => Promise.resolve({ data: 'mocked response' })), - // Add other methods as needed - })), - })); + /* + * Any updates to the store should be replicated here. + * */ + vi.mock('@/store', () => { + const useAuthStore = vi.fn(() => ({ + token: null, + username: null, + setToken: vi.fn(), + setUserName: vi.fn(), + })); + return { useAuthStore }; + }); + + /* If additional methods are added to api client, this wrapper needs to be updated + * As api-client is an abstraction of the underlying GitHub client, extending the app to other git managers + * will be easy. And this wrapper doesn't have to be updated. + * */ + + vi.mock('@/api', () => { + const getUserFn = vi.fn(() => Promise.resolve(createUserMock())); + const useUser = vi.fn(() => useUserMock()); + // const getReposFn = vi.fn(() => Promise.resolve(Array.from({ length: 10 }, createRepoMock))); + const useRepos = vi.fn(() => useReposMock()); + const useOAuth = vi.fn(() => ({ + data: { access_token: 'test' }, + isLoading: false, + isError: false, + })); + + const apiClient = { + get: vi.fn(() => Promise.resolve({ data: 'mocked get' })), + post: vi.fn(() => Promise.resolve({ data: 'mocked post' })), + put: vi.fn(() => Promise.resolve({ data: 'mocked put' })), + delete: vi.fn(() => Promise.resolve({ data: 'mocked delete' })), + patch: vi.fn(() => Promise.resolve({ data: 'mocked patch' })), + head: vi.fn(() => Promise.resolve({ data: 'mocked head' })), + options: vi.fn(() => Promise.resolve({ data: 'mocked options' })), + request: vi.fn(() => Promise.resolve({ data: 'mocked request' })), + interceptors: { + request: { use: vi.fn(), eject: vi.fn() }, + response: { use: vi.fn(), eject: vi.fn() }, + }, + defaults: { headers: { common: {} } }, + }; + + return { getUserFn, useUser, useRepos, apiClient, useOAuth, githubLogin: vi.fn() }; + }); + + vi.mock('react-router-dom', async (importOriginal) => { + const actual = await importOriginal(); // Import the actual module + return { + // @ts-ignore + ...actual, // Spread all original exports + useNavigate: vi.fn(), // Override specific exports you want to mock + }; + }); + + return testingLibraryRender(<>{ui}, { + wrapper: ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ), + }); +} + +export function renderWithOutMocks(ui: React.ReactNode) { return testingLibraryRender(<>{ui}, { - wrapper: ({ children }: { children: React.ReactNode }) => {children}, + wrapper: ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ), }); } diff --git a/ui/vite.config.mjs b/ui/vite.config.mjs index d225fb8d..e800cd15 100644 --- a/ui/vite.config.mjs +++ b/ui/vite.config.mjs @@ -1,12 +1,43 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tsconfigPaths from 'vite-tsconfig-paths'; +import istanbul from 'vite-plugin-istanbul'; export default defineConfig({ - plugins: [react(), tsconfigPaths()], + plugins: [react(), tsconfigPaths(), istanbul({ + include: [ + 'src/**/*.ts', + 'src/**/*.tsx', + ], + exclude: [ + 'node_modules/**', + 'tests/**', + '**/__*', + '**/*.mock.ts', + 'test-utils/__mocks__/**', + 'postcss.config.cjs', + ], + extension: [ '.ts', '.tsx' ], + cypress: false, + requireEnv: false + })], test: { globals: true, environment: 'jsdom', setupFiles: './vitest.setup.mjs', }, -}); \ No newline at end of file + coverage: { + provider: 'v8', + reporter: ['text', 'html'], + }, + server: { + proxy: { + '/login/oauth/access_token': { + target: 'https://github.com', + changeOrigin: true, + rewrite: path => path.replace(/^\/login\/oauth\/access_token/, '/login/oauth/access_token') + + } + } + } +}); diff --git a/ui/yarn.lock b/ui/yarn.lock index ac11e8cc..d5520c45 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -7,7 +7,7 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== -"@ampproject/remapping@^2.2.0": +"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.2.1": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== @@ -23,11 +23,24 @@ "@babel/highlight" "^7.24.2" picocolors "^1.0.0" +"@babel/code-frame@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.6.tgz#ab88da19344445c3d8889af2216606d3329f3ef2" + integrity sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA== + dependencies: + "@babel/highlight" "^7.24.6" + picocolors "^1.0.0" + "@babel/compat-data@^7.23.5": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== +"@babel/compat-data@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.6.tgz#b3600217688cabb26e25f8e467019e66d71b7ae2" + integrity sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ== + "@babel/core@^7.23.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.5.tgz#15ab5b98e101972d171aeef92ac70d8d6718f06a" @@ -49,6 +62,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.23.9": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.6.tgz#8650e0e4b03589ebe886c4e4a60398db0a7ec787" + integrity sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.6" + "@babel/generator" "^7.24.6" + "@babel/helper-compilation-targets" "^7.24.6" + "@babel/helper-module-transforms" "^7.24.6" + "@babel/helpers" "^7.24.6" + "@babel/parser" "^7.24.6" + "@babel/template" "^7.24.6" + "@babel/traverse" "^7.24.6" + "@babel/types" "^7.24.6" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.5.tgz#e5afc068f932f05616b66713e28d0f04e99daeb3" @@ -59,6 +93,16 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" +"@babel/generator@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.6.tgz#dfac82a228582a9d30c959fe50ad28951d4737a7" + integrity sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg== + dependencies: + "@babel/types" "^7.24.6" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/helper-compilation-targets@^7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" @@ -70,11 +114,27 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz#4a51d681f7680043d38e212715e2a7b1ad29cb51" + integrity sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg== + dependencies: + "@babel/compat-data" "^7.24.6" + "@babel/helper-validator-option" "^7.24.6" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-environment-visitor@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== +"@babel/helper-environment-visitor@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz#ac7ad5517821641550f6698dd5468f8cef78620d" + integrity sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g== + "@babel/helper-function-name@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" @@ -83,6 +143,14 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.23.0" +"@babel/helper-function-name@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz#cebdd063386fdb95d511d84b117e51fc68fec0c8" + integrity sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w== + dependencies: + "@babel/template" "^7.24.6" + "@babel/types" "^7.24.6" + "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" @@ -90,6 +158,13 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-hoist-variables@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz#8a7ece8c26756826b6ffcdd0e3cf65de275af7f9" + integrity sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA== + dependencies: + "@babel/types" "^7.24.6" + "@babel/helper-module-imports@^7.24.3": version "7.24.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128" @@ -97,6 +172,13 @@ dependencies: "@babel/types" "^7.24.0" +"@babel/helper-module-imports@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz#65e54ffceed6a268dc4ce11f0433b82cfff57852" + integrity sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g== + dependencies: + "@babel/types" "^7.24.6" + "@babel/helper-module-transforms@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz#ea6c5e33f7b262a0ae762fd5986355c45f54a545" @@ -108,6 +190,17 @@ "@babel/helper-split-export-declaration" "^7.24.5" "@babel/helper-validator-identifier" "^7.24.5" +"@babel/helper-module-transforms@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz#22346ed9df44ce84dee850d7433c5b73fab1fe4e" + integrity sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA== + dependencies: + "@babel/helper-environment-visitor" "^7.24.6" + "@babel/helper-module-imports" "^7.24.6" + "@babel/helper-simple-access" "^7.24.6" + "@babel/helper-split-export-declaration" "^7.24.6" + "@babel/helper-validator-identifier" "^7.24.6" + "@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz#a924607dd254a65695e5bd209b98b902b3b2f11a" @@ -120,6 +213,13 @@ dependencies: "@babel/types" "^7.24.5" +"@babel/helper-simple-access@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz#1d6e04d468bba4fc963b4906f6dac6286cfedff1" + integrity sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g== + dependencies: + "@babel/types" "^7.24.6" + "@babel/helper-split-export-declaration@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz#b9a67f06a46b0b339323617c8c6213b9055a78b6" @@ -127,21 +227,43 @@ dependencies: "@babel/types" "^7.24.5" +"@babel/helper-split-export-declaration@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz#e830068f7ba8861c53b7421c284da30ae656d7a3" + integrity sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw== + dependencies: + "@babel/types" "^7.24.6" + "@babel/helper-string-parser@^7.24.1": version "7.24.1" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== +"@babel/helper-string-parser@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz#28583c28b15f2a3339cfafafeaad42f9a0e828df" + integrity sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q== + "@babel/helper-validator-identifier@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62" integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA== +"@babel/helper-validator-identifier@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz#08bb6612b11bdec78f3feed3db196da682454a5e" + integrity sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw== + "@babel/helper-validator-option@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== +"@babel/helper-validator-option@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz#59d8e81c40b7d9109ab7e74457393442177f460a" + integrity sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ== + "@babel/helpers@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.5.tgz#fedeb87eeafa62b621160402181ad8585a22a40a" @@ -151,6 +273,14 @@ "@babel/traverse" "^7.24.5" "@babel/types" "^7.24.5" +"@babel/helpers@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.6.tgz#cd124245299e494bd4e00edda0e4ea3545c2c176" + integrity sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA== + dependencies: + "@babel/template" "^7.24.6" + "@babel/types" "^7.24.6" + "@babel/highlight@^7.24.2": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.5.tgz#bc0613f98e1dd0720e99b2a9ee3760194a704b6e" @@ -161,11 +291,26 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.0", "@babel/parser@^7.24.5": +"@babel/highlight@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.6.tgz#6d610c1ebd2c6e061cade0153bf69b0590b7b3df" + integrity sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ== + dependencies: + "@babel/helper-validator-identifier" "^7.24.6" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.0", "@babel/parser@^7.24.4", "@babel/parser@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.5.tgz#4a4d5ab4315579e5398a82dcf636ca80c3392790" integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg== +"@babel/parser@^7.23.9", "@babel/parser@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.6.tgz#5e030f440c3c6c78d195528c3b688b101a365328" + integrity sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q== + "@babel/plugin-transform-react-jsx-self@^7.23.3": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz#22cc7572947895c8e4cd034462e65d8ecf857756" @@ -180,7 +325,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.0" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== @@ -196,6 +341,15 @@ "@babel/parser" "^7.24.0" "@babel/types" "^7.24.0" +"@babel/template@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.6.tgz#048c347b2787a6072b24c723664c8d02b67a44f9" + integrity sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw== + dependencies: + "@babel/code-frame" "^7.24.6" + "@babel/parser" "^7.24.6" + "@babel/types" "^7.24.6" + "@babel/traverse@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.5.tgz#972aa0bc45f16983bf64aa1f877b2dd0eea7e6f8" @@ -212,6 +366,22 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.6.tgz#0941ec50cdeaeacad0911eb67ae227a4f8424edc" + integrity sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw== + dependencies: + "@babel/code-frame" "^7.24.6" + "@babel/generator" "^7.24.6" + "@babel/helper-environment-visitor" "^7.24.6" + "@babel/helper-function-name" "^7.24.6" + "@babel/helper-hoist-variables" "^7.24.6" + "@babel/helper-split-export-declaration" "^7.24.6" + "@babel/parser" "^7.24.6" + "@babel/types" "^7.24.6" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.0", "@babel/types@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.5.tgz#7661930afc638a5383eb0c4aee59b74f38db84d7" @@ -221,6 +391,20 @@ "@babel/helper-validator-identifier" "^7.24.5" to-fast-properties "^2.0.0" +"@babel/types@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.6.tgz#ba4e1f59870c10dc2fa95a274ac4feec23b21912" + integrity sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ== + dependencies: + "@babel/helper-string-parser" "^7.24.6" + "@babel/helper-validator-identifier" "^7.24.6" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + "@csstools/css-parser-algorithms@^2.6.1": version "2.6.3" resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.3.tgz#b5e7eb2bd2a42e968ef61484f1490a8a4148a8eb" @@ -410,6 +594,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@faker-js/faker@^8.4.1": + version "8.4.1" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.4.1.tgz#5d5e8aee8fce48f5e189bf730ebd1f758f491451" + integrity sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg== + "@floating-ui/core@^1.0.0": version "1.6.1" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.1.tgz#a4e6fef1b069cda533cbc7a4998c083a37f37573" @@ -465,6 +654,22 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@istanbuljs/load-nyc-config@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + "@jest/expect-utils@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" @@ -515,7 +720,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -548,6 +753,19 @@ resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.9.0.tgz#41ecc95d55da398c8c0718743f8cac13bf66b74d" integrity sha512-LKgyrlaIK0S/gcn/VDbhqLBZOYjvhXfVcH7rMs4MIBVD+wuRo2LvvAYe3cUfQbBCfmlpRjqvewwvsIYYsjSofQ== +"@mantine/notifications@^7.9.2": + version "7.9.2" + resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-7.9.2.tgz#dd1aa2f5d635b7c305d9c9738eb161bfaae12895" + integrity sha512-ERgmPLkiVPOqPjCVfSSK2QcRb/2W9wJVPpIlkSyMNYUWosceAH9uPhZCtnWxyRqH/PLhYtOOflxq2i4hiArEJQ== + dependencies: + "@mantine/store" "7.9.2" + react-transition-group "4.4.5" + +"@mantine/store@7.9.2": + version "7.9.2" + resolved "https://registry.yarnpkg.com/@mantine/store/-/store-7.9.2.tgz#1b20ba4acff3c085660e28e2daa11705ae1ddcba" + integrity sha512-oqCjse3cAp0DQI1fT5AWLW+Me6Mu4b2DVPpoRRwm7Ptw8gzUEmxb/9Brx2rkhaAym+S9sGe8IdEpNVLXaZyGXw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1034,6 +1252,25 @@ "@types/babel__core" "^7.20.5" react-refresh "^0.14.0" +"@vitest/coverage-v8@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz#2f54ccf4c2d9f23a71294aba7f95b3d2e27d14e7" + integrity sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew== + dependencies: + "@ampproject/remapping" "^2.2.1" + "@bcoe/v8-coverage" "^0.2.3" + debug "^4.3.4" + istanbul-lib-coverage "^3.2.2" + istanbul-lib-report "^3.0.1" + istanbul-lib-source-maps "^5.0.4" + istanbul-reports "^3.1.6" + magic-string "^0.30.5" + magicast "^0.3.3" + picocolors "^1.0.0" + std-env "^3.5.0" + strip-literal "^2.0.0" + test-exclude "^6.0.0" + "@vitest/expect@1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.6.0.tgz#0b3ba0914f738508464983f4d811bc122b51fb30" @@ -1149,6 +1386,13 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1388,6 +1632,11 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + camelize@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" @@ -1746,6 +1995,14 @@ dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + electron-to-chromium@^1.4.668: version "1.4.757" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.757.tgz#45f7c9341b538f8c4b9ca8af9692e0ed1a776a44" @@ -2092,6 +2349,11 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" + integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== + eslint@^8.57.0: version "8.57.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" @@ -2136,6 +2398,15 @@ eslint@^8.57.0: strip-ansi "^6.0.1" text-table "^0.2.0" +espree@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f" + integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww== + dependencies: + acorn "^8.11.3" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.0.0" + espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -2145,6 +2416,11 @@ espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" @@ -2261,6 +2537,14 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + find-up@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -2368,6 +2652,11 @@ get-nonce@^1.0.0: resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-stream@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" @@ -2396,7 +2685,7 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^7.1.3: +glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -2548,6 +2837,11 @@ html-encoding-sniffer@^4.0.0: dependencies: whatwg-encoding "^3.1.1" +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + html-tags@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" @@ -2858,6 +3152,48 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0, istanbul-lib-coverage@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz#91655936cf7380e4e473383081e38478b69993b1" + integrity sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw== + dependencies: + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz#1947003c72a91b6310efeb92d2a91be8804d92c2" + integrity sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.23" + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + +istanbul-reports@^3.1.6: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -2956,6 +3292,14 @@ js-tokens@^9.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.0.tgz#0f893996d6f3ed46df7f0a3b12a03f5fd84223c1" integrity sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ== +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -3097,6 +3441,13 @@ local-pkg@^0.5.0: mlly "^1.4.2" pkg-types "^1.0.3" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -3159,6 +3510,22 @@ magic-string@^0.30.5: dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" +magicast@^0.3.3: + version "0.3.4" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.4.tgz#bbda1791d03190a24b00ff3dd18151e7fd381d19" + integrity sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q== + dependencies: + "@babel/parser" "^7.24.4" + "@babel/types" "^7.24.0" + source-map-js "^1.2.0" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + mathml-tag-names@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" @@ -3214,7 +3581,7 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -3390,6 +3757,13 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -3404,6 +3778,13 @@ p-limit@^5.0.0: dependencies: yocto-queue "^1.0.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-locate@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" @@ -3411,6 +3792,11 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -3615,7 +4001,7 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -3736,6 +4122,16 @@ react-textarea-autosize@8.5.3: use-composed-ref "^1.3.0" use-latest "^1.2.1" +react-transition-group@4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -3909,6 +4305,11 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.5.3, semver@^7.5.4: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + semver@^7.6.0: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" @@ -3994,6 +4395,16 @@ source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + stack-utils@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" @@ -4279,6 +4690,15 @@ table@^6.8.2: string-width "^4.2.3" strip-ansi "^6.0.1" +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -4530,6 +4950,18 @@ vite-node@1.6.0: picocolors "^1.0.0" vite "^5.0.0" +vite-plugin-istanbul@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/vite-plugin-istanbul/-/vite-plugin-istanbul-6.0.2.tgz#242a57b20d540adc49631f9e6c174a1c3559ece7" + integrity sha512-0/sKwjEEIwbEyl43xX7onX3dIbMJAsigNsKyyVPalG1oRFo5jn3qkJbS2PUfp9wrr3piy1eT6qRoeeum2p4B2A== + dependencies: + "@istanbuljs/load-nyc-config" "^1.1.0" + espree "^10.0.1" + istanbul-lib-instrument "^6.0.2" + picocolors "^1.0.0" + source-map "^0.7.4" + test-exclude "^6.0.0" + vite-tsconfig-paths@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9"