diff --git a/bs3/package.json b/bs3/package.json index f8d326d..f8d1f46 100644 --- a/bs3/package.json +++ b/bs3/package.json @@ -2,7 +2,7 @@ "name": "bs3", "version": "0.1.0", "private": true, - "main": "src/index.js", + "main": "src/index.ts", "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", @@ -29,7 +29,14 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "7.21.11", - "env-cmd": "^10.1.0" + "@types/node": "^22.14.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.1", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "env-cmd": "^10.1.0", + "eslint": "^9.24.0", + "typescript": "^5.8.3" }, "scripts": { "start": "react-scripts start", diff --git a/bs3/src/App.test.js b/bs3/src/App.test.js deleted file mode 100644 index 1f03afe..0000000 --- a/bs3/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/bs3/src/App.js b/bs3/src/App.tsx similarity index 69% rename from bs3/src/App.js rename to bs3/src/App.tsx index 60cb16f..8e18ac4 100644 --- a/bs3/src/App.js +++ b/bs3/src/App.tsx @@ -1,16 +1,15 @@ +import React, { useEffect } from 'react'; import './App.css'; import BottomAppBar from './components/bottom-app-bar'; import { Outlet } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { setLocation } from './app/geolocatorSlice'; import useGeoLocationCheck from './api/geolocation'; -import { useEffect } from 'react'; function App() { const dispatch = useDispatch(); - const geoLocation = useGeoLocationCheck(); // Custom hook + const geoLocation = useGeoLocationCheck(); - // Use useEffect to dispatch the action only when geoLocation is valid useEffect(() => { if (geoLocation) { dispatch(setLocation(geoLocation)); @@ -20,9 +19,9 @@ function App() { return (
- +
); } -export default App; +export default App; \ No newline at end of file diff --git a/bs3/src/AppWrapper.js b/bs3/src/AppWrapper.js deleted file mode 100644 index 184ad3a..0000000 --- a/bs3/src/AppWrapper.js +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import App from './App'; -import { Box, Typography, Button, Modal } from '@mui/material'; - - -export default function AppWrapper() { - const dispatch = useDispatch(); - const [loading, setLoading] = useState(true); - const [showModal, setShowModal] = useState(true); - - const AppModal = ({ onClose }) => ( - - - - - Welcome to the BS3 community. - - - Location is optional but without it you will not be able to vote or post. - Location data will never be stored. - - - - - ); - - useEffect(() => { - if (!showModal) { - setLoading(false); - } - }, [dispatch, showModal]); - - const handleCloseModal = () => { - setShowModal(false); - }; - - if (showModal) { - return ; - } - - if (loading) { - return
Checking location...
; - } - - return ; -} \ No newline at end of file diff --git a/bs3/src/AppWrapper.tsx b/bs3/src/AppWrapper.tsx new file mode 100644 index 0000000..26be926 --- /dev/null +++ b/bs3/src/AppWrapper.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import App from './App'; +import { Box, Typography, Button, Modal } from '@mui/material'; + +interface AppModalProps { + onClose: () => void; +} + +const AppModal: React.FC = ({ onClose }) => { + const [showModal, setShowModal] = useState(true); + + return ( + + + + Welcome to the BS3 community. + + + Location is optional but without it you will not be able to vote or post. + Location data will never be stored. + + + + + ); +}; + +const AppWrapper: React.FC = () => { + const dispatch = useDispatch(); + const [loading, setLoading] = useState(true); + const [showModal, setShowModal] = useState(true); + + useEffect(() => { + if (!showModal) { + setLoading(false); + } + }, [dispatch, showModal]); + + const handleCloseModal = () => { + setShowModal(false); + }; + + if (showModal) { + return ; + } + + if (loading) { + return
Checking location...
; + } + + return ; +}; + +export default AppWrapper; \ No newline at end of file diff --git a/bs3/src/api/firebaseConfig.js b/bs3/src/api/firebaseConfig.js deleted file mode 100644 index 20f060e..0000000 --- a/bs3/src/api/firebaseConfig.js +++ /dev/null @@ -1,17 +0,0 @@ -import { initializeApp } from "firebase/app"; -import { getFirestore } from "firebase/firestore"; - -const firebaseConfig = { - apiKey: process.env.REACT_APP_FIREBASE_API_KEY, - authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, - projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, - storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.REACT_APP_FIREBASE_APP_ID, -}; - -// Initialize Firebase -const app = initializeApp(firebaseConfig); -const db = await getFirestore(app); - -export { db }; diff --git a/bs3/src/api/firebaseConfig.ts b/bs3/src/api/firebaseConfig.ts new file mode 100644 index 0000000..486ec51 --- /dev/null +++ b/bs3/src/api/firebaseConfig.ts @@ -0,0 +1,16 @@ +import { initializeApp } from "firebase/app"; +import { getFirestore } from "firebase/firestore"; + +const firebaseConfig = { + apiKey: process.env.REACT_APP_FIREBASE_API_KEY as string, + authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN as string, + projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID as string, + storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET as string, + messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID as string, + appId: process.env.REACT_APP_FIREBASE_APP_ID as string, +}; + +const app = initializeApp(firebaseConfig); +const db = getFirestore(app); + +export { db }; \ No newline at end of file diff --git a/bs3/src/api/geolocation.js b/bs3/src/api/geolocation.js deleted file mode 100644 index 20f804c..0000000 --- a/bs3/src/api/geolocation.js +++ /dev/null @@ -1,83 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; - -const options = { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0, -} - -const useGeoLocationCheck = () => { - const [location, setLocation] = useState({ - loaded: false, - coordinates: { lat: null, lng: null }, - local: false, - }); - - const isLocal = (location) => { - - const LNG_MIN = -2.6485976706726055; - const LNG_MAX = -2.5743893376966813; - const LAT_MIN = 51.42082059829641; - const LAT_MAX = 51.45543490194706; - - const lat = location.coords.latitude; - const lng = location.coords.longitude - - if (lat && lng) { - if ( lat >= LAT_MIN && lat <= LAT_MAX && lng >= LNG_MIN && lng <= LNG_MAX ) { - console.log("Local location detected"); - return true; - } else { - console.log("Non-local location detected"); - return false; - } - } else { - console.log("Location is not available"); - return false; - } - } - - const onSuccess = useCallback((location) => { - console.log("Location obtained: ", location); - setLocation({ - loaded: true, - coordinates: { - lat: location.coords.latitude, - lng: location.coords.longitude, - }, - local: isLocal(location), - }); - }, []); - - const onError = useCallback((error) => { - console.log("Error while obtaining geolocation: ", error); - setLocation({ - loaded: true, - error: { - code: error.code, - message: error.message, - }, - }); - }, []); - - useEffect(() => { - if (navigator.geolocation) { - navigator.permissions.query({ name: "geolocation" }).then((result) => { - if (result.state === "granted") { - navigator.geolocation.getCurrentPosition(onSuccess, onError, options); - } else if (result.state === "prompt") { - navigator.geolocation.getCurrentPosition(onSuccess, onError, options); - } else { - console.log("Geolocation permission denied"); - } - }); - } else { - console.log("Geolocation is not supported by this browser."); - } - }, [onSuccess, onError, location.loaded]); - - return location; - -}; - -export default useGeoLocationCheck; diff --git a/bs3/src/api/geolocation.ts b/bs3/src/api/geolocation.ts new file mode 100644 index 0000000..022b91c --- /dev/null +++ b/bs3/src/api/geolocation.ts @@ -0,0 +1,98 @@ +import { useState, useEffect, useCallback } from "react"; + +interface Coordinates { + lat: number | null; + lng: number | null; +} + +interface LocationState { + loaded: boolean; + coordinates: Coordinates; + local: boolean; + error?: { + code: number; + message: string; + }; +} + +const options: PositionOptions = { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, +}; + +const useGeoLocationCheck = (): LocationState => { + const [location, setLocation] = useState({ + loaded: false, + coordinates: { lat: null, lng: null }, + local: false, + }); + + const isLocal = (location: GeolocationPosition): boolean => { + const LNG_MIN = -2.6485976706726055; + const LNG_MAX = -2.5743893376966813; + const LAT_MIN = 51.42082059829641; + const LAT_MAX = 51.45543490194706; + + const lat = location.coords.latitude; + const lng = location.coords.longitude; + + if (lat && lng) { + if (lat >= LAT_MIN && lat <= LAT_MAX && lng >= LNG_MIN && lng <= LNG_MAX) { + console.log("Local location detected"); + return true; + } else { + console.log("Non-local location detected"); + return false; + } + } else { + console.log("Location is not available"); + return false; + } + }; + + const onSuccess = useCallback((location: GeolocationPosition): void => { + console.log("Location obtained: ", location); + setLocation({ + loaded: true, + coordinates: { + lat: location.coords.latitude, + lng: location.coords.longitude, + }, + local: isLocal(location), + }); + }, []); + + const onError = useCallback((error: GeolocationPositionError): void => { + console.log("Error while obtaining geolocation: ", error); + setLocation({ + loaded: true, + coordinates: { lat: null, lng: null }, + local: false, + error: { + code: error.code, + message: error.message, + }, + }); + }, []); + + useEffect(() => { + if (navigator.geolocation) { + navigator.permissions.query({ name: "geolocation" as PermissionName }).then((result) => { + if (result.state === "granted") { + navigator.geolocation.getCurrentPosition(onSuccess, onError, options); + } else if (result.state === "prompt") { + navigator.geolocation.getCurrentPosition(onSuccess, onError, options); + } else { + console.log("Geolocation permission denied"); + } + }); + } else { + console.log("Geolocation is not supported by this browser."); + } + }, [onSuccess, onError, location.loaded]); + + return location; +}; + +export default useGeoLocationCheck; diff --git a/bs3/src/app/firestoreSlice.js b/bs3/src/app/firestoreSlice.ts similarity index 52% rename from bs3/src/app/firestoreSlice.js rename to bs3/src/app/firestoreSlice.ts index 7e3e2d3..6225fde 100644 --- a/bs3/src/app/firestoreSlice.js +++ b/bs3/src/app/firestoreSlice.ts @@ -2,8 +2,28 @@ import { nanoid } from 'nanoid' import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { collection, addDoc, getDocs, updateDoc, deleteDoc, doc, arrayRemove, arrayUnion, getDoc } from "firebase/firestore"; import { db } from '../api/firebaseConfig'; +import { PostData, FirestoreState } from '../types/types'; -function rankingScore(upVotes, downVotes) { +const USER_ID_KEY = 'userId'; + +function loadOrCreateId(key: string, defaultValue: string): string { + const item = localStorage.getItem(key); + if (item) { + return item; + } else { + localStorage.setItem(key, defaultValue); + return defaultValue; + } +} + +const initialState: FirestoreState = { + user: loadOrCreateId(USER_ID_KEY, nanoid(10)), + posts: [], + status: 'idle', + error: null, +}; + +function rankingScore(upVotes: number, downVotes: number): number { const n = upVotes + downVotes; if (n === 0) return -1; @@ -14,57 +34,55 @@ function rankingScore(upVotes, downVotes) { return score; } -function loadOrCreateId(key, defaultValue) { - const item = localStorage.getItem(key) - if (item) { - return item - } else { - localStorage.setItem(key, defaultValue) - return defaultValue - } +function sortPostsByRanking(posts: PostData[]): PostData[] { + return posts.sort( + (a, b) => + rankingScore(b.upVoted.length, b.downVoted.length) - + rankingScore(a.upVoted.length, a.downVoted.length) + ); } -export const fetchPosts = createAsyncThunk( +// Fix typings for fetchPosts +export const fetchPosts = createAsyncThunk( 'firestore/fetchPosts', async () => { const querySnapshot = await getDocs(collection(db, 'posts')); - return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); + return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as PostData)); } ); -export const upVotePost = createAsyncThunk( +// Fix typings for upVotePost +export const upVotePost = createAsyncThunk( 'firestore/upVote', async ({ id, user }) => { - const post = doc(db, 'posts', id); - await updateDoc(post, { upVoted: arrayUnion(user), - downVoted: arrayRemove(user) + downVoted: arrayRemove(user), }); - const updatedPost = (await getDoc(post)).data() - return { id, ...updatedPost } + const updatedPost = (await getDoc(post)).data(); + return { id, ...updatedPost } as PostData; } ); -export const downVotePost = createAsyncThunk( +// Fix typings for downVotePost +export const downVotePost = createAsyncThunk( 'firestore/downVote', async ({ id, user }) => { - const post = doc(db, 'posts', id); - await updateDoc(post, { upVoted: arrayRemove(user), - downVoted: arrayUnion(user) + downVoted: arrayUnion(user), }); - const updatedPost = (await getDoc(post)).data() - return { id, ...updatedPost } + const updatedPost = (await getDoc(post)).data(); + return { id, ...updatedPost } as PostData; } ); -export const insertPost = createAsyncThunk( +// Fix typings for insertPost +export const insertPost = createAsyncThunk>( 'firestore/insertPost', async (data) => { const docRef = await addDoc(collection(db, 'posts'), data); @@ -72,50 +90,47 @@ export const insertPost = createAsyncThunk( } ); -export const updatePost = createAsyncThunk( +// Fix typings for updatePost +export const updatePost = createAsyncThunk }>( 'firestore/updatePost', async ({ id, data }) => { const docRef = doc(db, 'posts', id); - const serialised_data = JSON.parse(JSON.stringify(data)) + const serialised_data = JSON.parse(JSON.stringify(data)); await updateDoc(docRef, serialised_data); - return { id, ...data }; + return { id, ...data } as PostData; } ); +// Fix typings for deletePost export const deletePost = createAsyncThunk( 'posts/deletePost', - async (id, thunkAPI) => { + async (id: string) => { try { - console.log("Deleting post with ID:", id); const docRef = doc(db, 'posts', id); - console.log("Document reference:", docRef); await deleteDoc(docRef); - console.log("Document deleted successfully"); return id; } catch (error) { console.error("Error deleting document: ", error); - return thunkAPI.rejectWithValue({ error: error.message }); + return id } } ); const firestoreSlice = createSlice({ - name: 'firestore', - - initialState: { - user: loadOrCreateId("userId", nanoid(10)), - posts: [], - status: 'idle', - error: null - }, - + initialState, reducers: { sortPosts: (state) => { - state.posts.sort((a, b) => rankingScore(b.upVoted.length, b.downVoted.length) - rankingScore(a.upVoted.length, a.downVoted.length)) - } + state.posts.sort( + (a, b) => + rankingScore(b.upVoted.length, b.downVoted.length) - + rankingScore(a.upVoted.length, a.downVoted.length) + ); + }, + addPost: (state, action) => { + state.posts.push(action.payload); + }, }, - extraReducers: (builder) => { builder .addCase(fetchPosts.pending, (state) => { @@ -123,40 +138,39 @@ const firestoreSlice = createSlice({ }) .addCase(fetchPosts.fulfilled, (state, action) => { state.status = 'succeeded'; - state.posts = action.payload; - state.posts.sort((a, b) => rankingScore(b.upVoted.length, b.downVoted.length) - rankingScore(a.upVoted.length, a.downVoted.length)) + state.posts = sortPostsByRanking(action.payload); }) .addCase(fetchPosts.rejected, (state, action) => { state.status = 'failed'; - state.error = action.error.message; + state.error = action.error.message || null; }) .addCase(insertPost.fulfilled, (state, action) => { state.posts.push(action.payload); }) .addCase(upVotePost.fulfilled, (state, action) => { - const index = state.posts.findIndex(doc => doc.id === action.payload.id); + const index = state.posts.findIndex((doc) => doc.id === action.payload.id); if (index !== -1) { state.posts[index] = action.payload; } }) .addCase(downVotePost.fulfilled, (state, action) => { - const index = state.posts.findIndex(doc => doc.id === action.payload.id); + const index = state.posts.findIndex((doc) => doc.id === action.payload.id); if (index !== -1) { state.posts[index] = action.payload; } }) .addCase(updatePost.fulfilled, (state, action) => { - const index = state.posts.findIndex(doc => doc.id === action.payload.id); + const index = state.posts.findIndex((doc) => doc.id === action.payload.id); if (index !== -1) { state.posts[index] = action.payload; } }) .addCase(deletePost.fulfilled, (state, action) => { - state.posts = state.posts.filter(doc => doc.id !== action.payload); + state.posts = state.posts.filter((doc) => doc.id !== action.payload); }); - } + }, }); -export const { addPost, sortPosts } = firestoreSlice.actions +export const { addPost, sortPosts } = firestoreSlice.actions; export default firestoreSlice.reducer; diff --git a/bs3/src/app/geolocatorSlice.js b/bs3/src/app/geolocatorSlice.js deleted file mode 100644 index 4a259b2..0000000 --- a/bs3/src/app/geolocatorSlice.js +++ /dev/null @@ -1,21 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; - -export const geolocationSlice = createSlice({ - name: 'geolocation', - initialState: { - location: { - loaded: false, - coordinates: { lat: null, lng: null }, - local: false, - } - }, - reducers: { - setLocation: (state, action) => { - state.location = action.payload; - }, - }, -}); - -export const { setLocation } = geolocationSlice.actions; - -export default geolocationSlice.reducer; \ No newline at end of file diff --git a/bs3/src/app/geolocatorSlice.ts b/bs3/src/app/geolocatorSlice.ts new file mode 100644 index 0000000..df710ed --- /dev/null +++ b/bs3/src/app/geolocatorSlice.ts @@ -0,0 +1,35 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface Location { + loaded: boolean; + coordinates: { + lat: number | null; + lng: number | null; + }; + local: boolean; +} + +interface GeolocationState { + location: Location; +} + +const initialState: GeolocationState = { + location: { + loaded: false, + coordinates: { lat: null, lng: null }, + local: false, + }, +}; + +export const geolocationSlice = createSlice({ + name: 'geolocation', + initialState, + reducers: { + setLocation: (state, action: PayloadAction) => { + state.location = action.payload; + }, + }, +}); + +export const { setLocation } = geolocationSlice.actions; +export default geolocationSlice.reducer; \ No newline at end of file diff --git a/bs3/src/app/hooks.ts b/bs3/src/app/hooks.ts new file mode 100644 index 0000000..99acaa9 --- /dev/null +++ b/bs3/src/app/hooks.ts @@ -0,0 +1,5 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; \ No newline at end of file diff --git a/bs3/src/app/store.js b/bs3/src/app/store.js deleted file mode 100644 index 1ae1f94..0000000 --- a/bs3/src/app/store.js +++ /dev/null @@ -1,12 +0,0 @@ -import { configureStore } from '@reduxjs/toolkit' -import firestoreReducer, { fetchPosts } from './firestoreSlice' -import geolocatorSlice from './geolocatorSlice' - -export const store = configureStore({ - reducer: { - app: firestoreReducer, - geolocation: geolocatorSlice - } -}) - -store.dispatch(fetchPosts()) \ No newline at end of file diff --git a/bs3/src/app/store.ts b/bs3/src/app/store.ts new file mode 100644 index 0000000..677b5f1 --- /dev/null +++ b/bs3/src/app/store.ts @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit'; +import firestoreReducer, { fetchPosts } from './firestoreSlice'; +import geolocatorReducer from './geolocatorSlice'; + +export const store = configureStore({ + reducer: { + app: firestoreReducer, + geolocation: geolocatorReducer, + }, +}); + +store.dispatch(fetchPosts()); + +// Optional: export types for use with TypeScript elsewhere +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; \ No newline at end of file diff --git a/bs3/src/components/bottom-app-bar.js b/bs3/src/components/bottom-app-bar.js deleted file mode 100644 index 2786db0..0000000 --- a/bs3/src/components/bottom-app-bar.js +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from 'react'; -import AppBar from '@mui/material/AppBar'; -import Box from '@mui/material/Box'; -import CssBaseline from '@mui/material/CssBaseline'; -import Toolbar from '@mui/material/Toolbar'; -import IconButton from '@mui/material/IconButton'; -import MenuIcon from '@mui/icons-material/Menu'; -import useScrollTrigger from '@mui/material/useScrollTrigger'; -import Slide from '@mui/material/Slide'; -import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux' -import { Typography } from '@mui/material'; -import CheckIcon from '@mui/icons-material/Check'; -import CrossIcon from '@mui/icons-material/Close'; - -function HideOnScroll(props) { - const { children } = props; - // Note that you normally won't need to set the window ref as useScrollTrigger - // will default to window. - // This is only being set here because the demo is in an iframe. - const trigger = useScrollTrigger(); - - return ( - - {children} - - ); - } - - HideOnScroll.propTypes = { - children: PropTypes.element.isRequired, - }; - - export default function BottomAppBar() { - - const user = useSelector(state => state.app.user) - const location = useSelector(state => state.geolocation.location) - - return ( - - - - - - - - - - - - User ID - {user} - -
- { location.local ? ( - - Local mode - - - - ) : ( - - Guest mode - - - - )} -
-
-
-
-
- ); - - } \ No newline at end of file diff --git a/bs3/src/components/bottom-app-bar.tsx b/bs3/src/components/bottom-app-bar.tsx new file mode 100644 index 0000000..c33ff7b --- /dev/null +++ b/bs3/src/components/bottom-app-bar.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import CssBaseline from '@mui/material/CssBaseline'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import MenuIcon from '@mui/icons-material/Menu'; +import useScrollTrigger from '@mui/material/useScrollTrigger'; +import Slide from '@mui/material/Slide'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { Typography } from '@mui/material'; +import CheckIcon from '@mui/icons-material/Check'; +import CrossIcon from '@mui/icons-material/Close'; + +// Define the props for the HideOnScroll component +interface HideOnScrollProps { + children: React.ReactElement; +} + +function HideOnScroll(props: HideOnScrollProps): JSX.Element { + const { children } = props; + const trigger = useScrollTrigger(); + + return ( + + {children} + + ); +} + +HideOnScroll.propTypes = { + children: PropTypes.element.isRequired, +}; + +// Define the types for the Redux state +interface AppState { + app: { + user: string; + }; + geolocation: { + location: { + local: boolean; + }; + }; +} + +export default function BottomAppBar(): JSX.Element { + const user = useSelector((state: AppState) => state.app.user); + const location = useSelector((state: AppState) => state.geolocation.location); + + return ( + + + + + + + + + + + + User ID - {user} + +
+ {location.local ? ( + + + Local mode + + + + ) : ( + + + Guest mode + + + + )} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/bs3/src/components/main-action-buttons.js b/bs3/src/components/main-action-buttons.js deleted file mode 100644 index 0bfddfc..0000000 --- a/bs3/src/components/main-action-buttons.js +++ /dev/null @@ -1,45 +0,0 @@ -import { styled } from '@mui/material/styles'; -import Fab from '@mui/material/Fab'; -import { Link } from 'react-router-dom'; -import AddIcon from '@mui/icons-material/Add'; -import SendIcon from '@mui/icons-material/Send'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; - -const StyledFab = styled(Fab)({ - position: 'fixed', - zIndex: 1300, - bottom: 30, - left: 0, - right: 0, - margin: '0 auto', -}); - - -export default function MainActionFab({ type }) { - switch(type) { - - case "new": - return ( - - - - ) - - case "submit": - return ( - - - - ) - - case "back": - return ( - - - - ) - - default: - - } -} diff --git a/bs3/src/components/main-action-buttons.tsx b/bs3/src/components/main-action-buttons.tsx new file mode 100644 index 0000000..840ec1a --- /dev/null +++ b/bs3/src/components/main-action-buttons.tsx @@ -0,0 +1,51 @@ +import { styled } from '@mui/material/styles'; +import Fab from '@mui/material/Fab'; +import { Link } from 'react-router-dom'; +import AddIcon from '@mui/icons-material/Add'; +import SendIcon from '@mui/icons-material/Send'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + +const StyledFab = styled(Fab)({ + position: 'fixed', + zIndex: 1300, + bottom: 30, + left: 0, + right: 0, + margin: '0 auto', +}); + +interface MainActionFabProps { + type: 'new' | 'submit' | 'back'; +} + +export default function MainActionFab({ type }: MainActionFabProps): JSX.Element | null { + switch (type) { + case 'new': + return ( + + + + + + ); + + case 'submit': + return ( + + + + ); + + case 'back': + return ( + + + + + + ); + + default: + return null; + } +} diff --git a/bs3/src/components/post-card.js b/bs3/src/components/post-card.tsx similarity index 50% rename from bs3/src/components/post-card.js rename to bs3/src/components/post-card.tsx index 295f5e3..6637bc3 100644 --- a/bs3/src/components/post-card.js +++ b/bs3/src/components/post-card.tsx @@ -11,129 +11,134 @@ import ArrowCircleUpIcon from '@mui/icons-material/ArrowCircleUp'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import { Link, useNavigate } from 'react-router-dom'; -import { useSelector, useDispatch } from 'react-redux' -import { upVotePost, downVotePost, sortPosts } from '../app/firestoreSlice'; +import { useAppDispatch, useAppSelector } from '../app/hooks'; +import { upVotePost, downVotePost, sortPosts, deletePost } from '../app/firestoreSlice'; import { useState } from 'react' import ReactMarkdown from 'react-markdown'; -import { deletePost } from '../app/firestoreSlice'; +import { PostData } from '../types/types'; -function ConditionalLink({ children, condition, ...props }) { - return !!condition && props.to ? - - {children} - : <>{children} +// Define types for ConditionalLink props +interface ConditionalLinkProps { + children: React.ReactNode; + condition: boolean; + to: string; + [key: string]: any; // Allow additional props } -function timeAgo(timestamp) { - const now = new Date(); - const postDate = new Date(timestamp); - const diffInSeconds = Math.floor((now - postDate) / 1000); +function ConditionalLink({ children, condition, ...props }: ConditionalLinkProps) { + return !!condition && props.to ? ( + + {children} + + ) : ( + <>{children} + ); +} - if (diffInSeconds < 60) { - return 'now'; - } else if (diffInSeconds < 3600) { - const minutes = Math.floor(diffInSeconds / 60); - return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; - } else if (diffInSeconds < 86400) { - const hours = Math.floor(diffInSeconds / 3600); - return `${hours} hour${hours > 1 ? 's' : ''} ago`; - } else { - const days = Math.floor(diffInSeconds / 86400); - return `${days} day${days > 1 ? 's' : ''} ago`; - } +interface BasicCardProps { + post: PostData; + extended?: boolean; } +interface ThisPostState { + userUpVoted: boolean; + userDownVoted: boolean; +} -export default function BasicCard({ post, extended = false}) { - const dispatch = useDispatch() - const navigate = useNavigate() - - const userId = useSelector(state => state.app.user) - const location = useSelector(state => state.geolocation.location) - const [deleteWarningOpen, setDeleteWarningOpen] = useState(false); +export default function BasicCard({ post, extended = false }: BasicCardProps) { - const [thisPost, setThisPost] = useState({ + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const [deleteWarningOpen, setDeleteWarningOpen] = useState(false); + + const userId = useAppSelector(state => state.app.user); + const location = useAppSelector(state => state.geolocation.location); + + const [thisPost, setThisPost] = useState({ userUpVoted: post.upVoted.includes(userId), - userDownVoted: post.downVoted.includes(userId) - }) - + userDownVoted: post.downVoted.includes(userId), + }); + const handleEdit = () => { localStorage.setItem('newPostForm', JSON.stringify(post)); - navigate("/new") + navigate('/new'); }; const handleDelete = () => { - setDeleteWarningOpen(true) + setDeleteWarningOpen(true); }; - + const handleConfirmDelete = () => { - dispatch(deletePost(post.id)) - navigate("/") - setDeleteWarningOpen(false) - } + dispatch(deletePost(post.id)); + navigate('/'); + setDeleteWarningOpen(false); + }; const handleClose = () => { setDeleteWarningOpen(false); }; - function setUpVoted() { + const setUpVoted = () => { setThisPost({ userUpVoted: true, - userDownVoted: false - }) - } + userDownVoted: false, + }); + }; - function setDownVoted() { + const setDownVoted = () => { setThisPost({ userUpVoted: false, - userDownVoted: true - }) - } + userDownVoted: true, + }); + }; - if (extended === false) { - post = {...post, body: post.body.replace(/\n/g, ' ').slice(0, 75) + "..."} - } - - function handleUpVote() { - dispatch(upVotePost({ id: post.id, user: userId})) - dispatch(sortPosts()) - setUpVoted() + if (!extended) { + post = { ...post, body: post.body.replace(/\n/g, ' ').slice(0, 75) + '...' }; } - function handleDownVote() { - dispatch(downVotePost({ id: post.id, user: userId})) - dispatch(sortPosts()) - setDownVoted() - } - + const handleUpVote = () => { + dispatch(upVotePost({ id: post.id, user: userId })); + dispatch(sortPosts()); + setUpVoted(); + }; + + const handleDownVote = () => { + dispatch(downVotePost({ id: post.id, user: userId })); + dispatch(sortPosts()); + setDownVoted(); + }; + return ( - - + + {post.userId === userId && extended && (
@@ -170,47 +175,51 @@ export default function BasicCard({ post, extended = false}) { {timeAgo(post.timestamp)} - - + + - - - -
-
- - - Details - - # - - ), - }}/> - - - - ), - }}/> - - - - ), - }}/> - Body -
- formik.setFieldTouched('body', true)} - options={editorOptions} - /> - {formik.touched.body && formik.errors.body ? ( - {formik.errors.body} - ) : null} -
-
- -
-
- - ) : ( - - - - Oops - - - You do not seem to be in the local area. You are in guest mode. -
-
- You cannot submit a post. -
-
- Try refreshing the page and allowing access to your devices location. -
-
- I will check your location once. You are free to remain anonymous and your location data will never be stored. -
- -
-
- )} - - ) - -} \ No newline at end of file diff --git a/bs3/src/pages/new-post-form.tsx b/bs3/src/pages/new-post-form.tsx new file mode 100644 index 0000000..e61dba3 --- /dev/null +++ b/bs3/src/pages/new-post-form.tsx @@ -0,0 +1,269 @@ +import React, { + useState, + useCallback, + useMemo, + useEffect, + useRef, + FormEvent, +} from "react"; +import { + Paper, + TextField, + Stack, + FormHelperText, + InputAdornment, + FormGroup, + Modal, + Box, + Button, + Typography, +} from "@mui/material"; +import AccountCircle from "@mui/icons-material/AccountCircle"; +import TitleIcon from "@mui/icons-material/Title"; +import { useNavigate } from "react-router-dom"; +import { useFormik } from "formik"; +import * as yup from "yup"; +import { useAppSelector, useAppDispatch } from "../app/hooks"; +import { insertPost } from "../app/firestoreSlice"; +import MainActionFab from "../components/main-action-buttons"; +import SimpleMDEEditor from "react-simplemde-editor"; +import { Options } from "easymde"; +import "easymde/dist/easymde.min.css"; + +interface FormValues { + tag: string; + author: string; + title: string; + body: string; + upVoted: string[]; + downVoted: string[]; +} + +const validationSchema = yup.object({ + tag: yup + .string() + .matches(/^\S*$/, "Tag must not contain spaces") + .matches(/^[a-zA-Z0-9]+$/, "Tag must contain only letters and numbers") + .max(16, "Tag must be 16 characters or less"), + author: yup.string().default("Anonymous"), + title: yup + .string() + .min(8, "Must be 8 characters long") + .max(32, "Must be 32 characters or less.") + .required("Title is required"), + body: yup + .string() + .min(32, "Body must be 32 characters long.") + .required("Body is required"), +}); + +export default function PostForm(): JSX.Element { + + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + + const userId = useAppSelector((state) => state.app.user); + const location = useAppSelector((state) => state.geolocation.location); + + const [open, setOpen] = useState(!location.local); + const hasLoadedFromLocalStorage = useRef(false); + + const formik = useFormik({ + initialValues: { + tag: "", + author: "", + title: "", + body: "", + upVoted: [], + downVoted: [], + }, + validationSchema, + onSubmit: (values: FormValues) => { + handleSubmit(values); + }, + }); + + function handleSubmit(values: FormValues): void { + const newPost = { + ...values, + userId, + timestamp: new Date().toISOString(), + }; + if (!newPost.author) { + newPost.author = "anonymous"; + } + const submittedPost = dispatch(insertPost(newPost)) as any; + navigate("/", { state: { postId: submittedPost.id } }); + } + + function handleClear(): void { + formik.resetForm(); + } + + const handleClose = (): void => { + setOpen(false); + navigate(-1); + }; + + const handleEditorChange = useCallback( + (value: string) => { + formik.setFieldValue("body", value); + }, + [formik] + ); + + const editorOptions: Options = useMemo( + () => ({ + toolbar: [ + "bold", + "italic", + "heading", + "|", + "quote", + "unordered-list", + "ordered-list", + "|", + "link", + "preview", + "side-by-side", + "fullscreen", + ], + }), + [] + ); + + useEffect(() => { + if (!hasLoadedFromLocalStorage.current) { + const savedForm = localStorage.getItem("newPostForm"); + if (savedForm) { + formik.setValues(JSON.parse(savedForm)); + } + hasLoadedFromLocalStorage.current = true; + } + }, [formik]); + + useEffect(() => { + localStorage.setItem("newPostForm", JSON.stringify(formik.values)); + }, [formik.values]); + + return ( + <> + {location.local ? ( + +
+ + +
+
+ + + Details + # + ), + }} + /> + + + + ), + }} + /> + + + + ), + }} + /> + Body +
+ formik.setFieldTouched("body", true)} + options={editorOptions} + /> + {formik.touched.body && formik.errors.body ? ( + + {formik.errors.body} + + ) : null} +
+
+ +
+
+
+ ) : ( + + + + Oops + + + You do not seem to be in the local area. You are in guest mode. +
+
+ You cannot submit a post. +
+
+ Try refreshing the page and allowing access to your devices + location. +
+
+ I will check your location once. You are free to remain anonymous + and your location data will never be stored. +
+ +
+
+ )} + + ); +} \ No newline at end of file diff --git a/bs3/src/pages/post-detail.js b/bs3/src/pages/post-detail.js deleted file mode 100644 index 501f33d..0000000 --- a/bs3/src/pages/post-detail.js +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react'; -import { useLoaderData } from "react-router-dom"; -import BasicCard from '../components/post-card'; -import { useSelector } from 'react-redux' -import MainActionFab from '../components/main-action-buttons'; - -export function idLoader({ params }) { - return params.postId; -} - -function Post() { - var postId = useLoaderData(); - const post = useSelector(state => state.app.posts.find(post => post.id === postId)) - - return ( -
- - -
- ) -} - -export default Post; \ No newline at end of file diff --git a/bs3/src/pages/post-detail.tsx b/bs3/src/pages/post-detail.tsx new file mode 100644 index 0000000..ec8cea7 --- /dev/null +++ b/bs3/src/pages/post-detail.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { useLoaderData } from "react-router-dom"; +import BasicCard from "../components/post-card"; +import { useSelector } from "react-redux"; +import MainActionFab from "../components/main-action-buttons"; +import { PostData } from "../types/types"; +import { RootState } from "../app/store"; +import type { LoaderFunction } from '@remix-run/router'; + +// Define the argument shape for the loader function +interface LoaderArgs { + params: { + postId?: string; + }; +} + +// Loader function that retrieves the post ID +export const idLoader: LoaderFunction = ({ params }: LoaderArgs) => { + if (!params.postId) { + throw new Response("Post ID not found", { status: 404 }); + } + return params.postId; +}; + +function Post(): JSX.Element { + const postId = useLoaderData() as string; + const post = useSelector((state: RootState) => + state.app.posts.find((item) => item.id === postId) + ); + + if (!post) { + return
Post not found
; + } + + return ( +
+ + +
+ ); +} + +export default Post; \ No newline at end of file diff --git a/bs3/src/pages/post-list.js b/bs3/src/pages/post-list.js deleted file mode 100644 index b20a836..0000000 --- a/bs3/src/pages/post-list.js +++ /dev/null @@ -1,131 +0,0 @@ -import React from "react"; -import BasicCard from "../components/post-card"; -import Button from '@mui/material/Button'; -import Snackbar from '@mui/material/Snackbar'; -import IconButton from '@mui/material/IconButton'; -import CloseIcon from '@mui/icons-material/Close'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import { useLocation, Link } from 'react-router-dom'; -import { useState } from "react"; -import { useSelector, useDispatch } from 'react-redux' -import MainActionFab from "../components/main-action-buttons"; -import PullToRefresh from 'react-simple-pull-to-refresh'; -import { fetchPosts, sortPosts } from "../app/firestoreSlice"; - - -class PostList extends React.Component { - constructor(posts) { - super() - } - - render() { - return this.props.posts.map(p => { - return ( -
- -
- ) - }) - } -} - -export default function PostListPage() { - const dispatch = useDispatch() - const posts = useSelector(state => state.app.posts) - const location = useLocation(); - var snackBar = { - open: false, - postId: null - } - - if (location.state !== null) { - snackBar = { - open: true, - postId: location.state.postId - } - window.history.replaceState({}, document.title) - } - - const handleRefresh = () => { - return new Promise((resolve) => { - dispatch(fetchPosts()); - dispatch(sortPosts()); - resolve(); - }); - }; - - return ( -
- - handleRefresh()} - pullingContent={ -
- -
- } - refreshingContent={ -
- -
- } - > - -
- -
- ) -} - - -function SimpleSnackbar({snackbar}) { - const [snack, setSnack] = useState({ - open: snackbar.open, - postId: snackbar.postId - }); - - const handleClose = (event, reason) => { - if (reason === 'clickaway') { - return; - } - setSnack({...snack, ...{open: false}}); - }; - - const action = ( - - - - - - - ); - - return ( -
- - -
- ); -} \ No newline at end of file diff --git a/bs3/src/pages/post-list.tsx b/bs3/src/pages/post-list.tsx new file mode 100644 index 0000000..e834203 --- /dev/null +++ b/bs3/src/pages/post-list.tsx @@ -0,0 +1,146 @@ +import React, { useState, MouseEvent } from "react"; +import BasicCard from "../components/post-card"; +import Button from '@mui/material/Button'; +import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { useLocation, Link } from 'react-router-dom'; +import MainActionFab from "../components/main-action-buttons"; +import PullToRefresh from 'react-simple-pull-to-refresh'; +import { fetchPosts, sortPosts } from "../app/firestoreSlice"; +import { PostData } from "../types/types"; +import { RootState } from "../app/store"; +import { useAppDispatch, useAppSelector } from "../app/hooks"; + +// Props for the PostList class component +interface PostListProps { + posts: PostData[]; +} + +class PostList extends React.Component { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(props: PostListProps) { + super(props); + } + + render() { + return this.props.posts.map((p) => ( +
+ +
+ )); + } +} + +interface SnackBarState { + open: boolean; + postId: string | null; +} + +// PostListPage component +export default function PostListPage(): JSX.Element { + const dispatch = useAppDispatch(); + const posts = useAppSelector((state: RootState) => state.app.posts); + const location = useLocation(); + let snackBar: SnackBarState = { open: false, postId: null }; + + if (location.state !== null) { + const { postId } = (location.state as { postId?: string }) || {}; + snackBar = { + open: !!postId, + postId: postId || null + }; + window.history.replaceState({}, document.title); + } + + const handleRefresh = () => { + return new Promise((resolve) => { + dispatch(fetchPosts()); + dispatch(sortPosts()); + resolve(); + }); + }; + + return ( +
+ + handleRefresh()} + pullingContent={ +
+ +
+ } + refreshingContent={ +
+ +
+ } + > + +
+ +
+ ); +} + +// Props for SimpleSnackbar +interface SimpleSnackbarProps { + snackbar: SnackBarState; +} + +function SimpleSnackbar({ snackbar }: SimpleSnackbarProps): JSX.Element { + const [snack, setSnack] = useState({ + open: snackbar.open, + postId: snackbar.postId + }); + + const handleClose = ( + event: Event | React.SyntheticEvent, + reason?: SnackbarCloseReason + ) => { + if (reason === 'clickaway') { + return; + } + setSnack({ ...snack, open: false }); + }; + + const action = ( + <> + + + + + + ); + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/bs3/src/reportWebVitals.js b/bs3/src/reportWebVitals.js deleted file mode 100644 index 5253d3a..0000000 --- a/bs3/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/bs3/src/setupTests.js b/bs3/src/setupTests.js deleted file mode 100644 index 8f2609b..0000000 --- a/bs3/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/bs3/src/types/types.ts b/bs3/src/types/types.ts new file mode 100644 index 0000000..ed07a26 --- /dev/null +++ b/bs3/src/types/types.ts @@ -0,0 +1,24 @@ +export type PostData = { + id: string; + title: string; + body: string; + author: string; + tag: string; + upVoted: string[]; + downVoted: string[]; + timestamp: string; + userId: string; +}; + +export type FirestoreState = { + user: string; + posts: PostData[]; + status: 'idle' | 'loading' | 'succeeded' | 'failed'; + error: string | null; +}; + +export interface BasicCardProps { + post: PostData; + extended?: boolean; +}; + diff --git a/bs3/tsconfig.json b/bs3/tsconfig.json new file mode 100644 index 0000000..304802b --- /dev/null +++ b/bs3/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "ESNext", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src"], + "exclude": ["node_modules", "build"] +} \ No newline at end of file diff --git a/bs3/yarn.lock b/bs3/yarn.lock index 55ff17f..1e5863d 100644 --- a/bs3/yarn.lock +++ b/bs3/yarn.lock @@ -1387,11 +1387,51 @@ dependencies: eslint-visitor-keys "^3.3.0" +"@eslint-community/eslint-utils@^4.4.0": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz#b0fc7e06d0c94f801537fd4237edc2706d3b8e4c" + integrity sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + "@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.9.1.tgz#449dfa81a57a1d755b09aa58d826c1262e4283b4" integrity sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA== +"@eslint/config-array@^0.20.0": + version "0.20.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.20.0.tgz#7a1232e82376712d3340012a2f561a2764d1988f" + integrity sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ== + dependencies: + "@eslint/object-schema" "^2.1.6" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/config-helpers@^0.2.0": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.1.tgz#26042c028d1beee5ce2235a7929b91c52651646d" + integrity sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw== + +"@eslint/core@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.12.0.tgz#5f960c3d57728be9f6c65bd84aa6aa613078798e" + integrity sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/core@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.13.0.tgz#bf02f209846d3bf996f9e8009db62df2739b458c" + integrity sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw== + dependencies: + "@types/json-schema" "^7.0.15" + "@eslint/eslintrc@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" @@ -1407,11 +1447,44 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/eslintrc@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964" + integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + "@eslint/js@8.51.0": version "8.51.0" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.51.0.tgz#6d419c240cfb2b66da37df230f7e7eef801c32fa" integrity sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg== +"@eslint/js@9.24.0": + version "9.24.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.24.0.tgz#685277980bb7bf84ecc8e4e133ccdda7545a691e" + integrity sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA== + +"@eslint/object-schema@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" + integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== + +"@eslint/plugin-kit@^0.2.7": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz#47488d8f8171b5d4613e833313f3ce708e3525f8" + integrity sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA== + dependencies: + "@eslint/core" "^0.13.0" + levn "^0.4.1" + "@firebase/analytics-compat@0.2.6": version "0.2.6" resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz#50063978c42f13eb800e037e96ac4b17236841f4" @@ -1829,6 +1902,19 @@ protobufjs "^7.2.4" yargs "^17.7.2" +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" + "@humanwhocodes/config-array@^0.11.11": version "0.11.11" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" @@ -1848,6 +1934,16 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.2.tgz#1860473de7dfa1546767448f333db80cb0ff2161" + integrity sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -2701,6 +2797,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/estree@^1.0.6": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": version "4.17.37" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz#7e4b7b59da9142138a2aaa7621f5abedce8c7320" @@ -2792,6 +2893,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -2838,6 +2944,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^22.14.0": + version "22.14.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.0.tgz#d3bfa3936fef0dbacd79ea3eb17d521c628bb47e" + integrity sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA== + dependencies: + undici-types "~6.21.0" + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -2880,6 +2993,11 @@ dependencies: "@types/react" "*" +"@types/react-dom@^19.1.1": + version "19.1.1" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.1.tgz#a8d097b28247d1129cf56e74d1622c98978c04ed" + integrity sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w== + "@types/react-transition-group@^4.4.7": version "4.4.8" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.8.tgz#46f87d80512959cac793ecc610a93d80ef241ccf" @@ -2896,6 +3014,13 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^19.1.0": + version "19.1.0" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.0.tgz#73c43ad9bc43496ca8184332b111e2aef63fc9da" + integrity sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w== + dependencies: + csstype "^3.0.2" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -3030,6 +3155,21 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/eslint-plugin@^8.29.1": + version "8.29.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz#593639d9bb5239b2d877d65757b7e2c9100a2e84" + integrity sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.29.1" + "@typescript-eslint/type-utils" "8.29.1" + "@typescript-eslint/utils" "8.29.1" + "@typescript-eslint/visitor-keys" "8.29.1" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^2.0.1" + "@typescript-eslint/experimental-utils@^5.0.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz#14559bf73383a308026b427a4a6129bae2146741" @@ -3047,6 +3187,17 @@ "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" +"@typescript-eslint/parser@^8.29.1": + version "8.29.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.29.1.tgz#10bf37411be0a199c27b6515726e22fe8d3df8d0" + integrity sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg== + dependencies: + "@typescript-eslint/scope-manager" "8.29.1" + "@typescript-eslint/types" "8.29.1" + "@typescript-eslint/typescript-estree" "8.29.1" + "@typescript-eslint/visitor-keys" "8.29.1" + debug "^4.3.4" + "@typescript-eslint/scope-manager@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" @@ -3055,6 +3206,14 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" +"@typescript-eslint/scope-manager@8.29.1": + version "8.29.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz#cfdfd4144f20c38b9d3e430efd6480e297ef52f6" + integrity sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA== + dependencies: + "@typescript-eslint/types" "8.29.1" + "@typescript-eslint/visitor-keys" "8.29.1" + "@typescript-eslint/type-utils@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" @@ -3065,11 +3224,26 @@ debug "^4.3.4" tsutils "^3.21.0" +"@typescript-eslint/type-utils@8.29.1": + version "8.29.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz#653dfff5c1711bc920a6a46a5a2c274899f00179" + integrity sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw== + dependencies: + "@typescript-eslint/typescript-estree" "8.29.1" + "@typescript-eslint/utils" "8.29.1" + debug "^4.3.4" + ts-api-utils "^2.0.1" + "@typescript-eslint/types@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== +"@typescript-eslint/types@8.29.1": + version "8.29.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.29.1.tgz#984ed1283fedbfb41d3993a9abdcb7b299971500" + integrity sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ== + "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" @@ -3083,6 +3257,20 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@8.29.1": + version "8.29.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz#4ac085665ed5390d11c0e3426427978570e3b747" + integrity sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g== + dependencies: + "@typescript-eslint/types" "8.29.1" + "@typescript-eslint/visitor-keys" "8.29.1" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.0.1" + "@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.58.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" @@ -3097,6 +3285,16 @@ eslint-scope "^5.1.1" semver "^7.3.7" +"@typescript-eslint/utils@8.29.1": + version "8.29.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.29.1.tgz#3d206c8c8def3527a8eb0588e94e3e60f7e167c9" + integrity sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.29.1" + "@typescript-eslint/types" "8.29.1" + "@typescript-eslint/typescript-estree" "8.29.1" + "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" @@ -3105,6 +3303,14 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@8.29.1": + version "8.29.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz#9b74e5098c71138d42bbf2178fbe4dfad45d6b9a" + integrity sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg== + dependencies: + "@typescript-eslint/types" "8.29.1" + eslint-visitor-keys "^4.2.0" + "@ungap/structured-clone@^1.0.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -3282,6 +3488,11 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.14.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + acorn@^8.2.4, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" @@ -3824,6 +4035,13 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -4279,6 +4497,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -4521,6 +4748,13 @@ debug@^4.0.0: dependencies: ms "2.1.2" +debug@^4.3.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + decimal.js@^10.2.1: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -5213,6 +5447,14 @@ eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" +eslint-scope@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.3.0.tgz#10cd3a918ffdd722f5f3f7b5b83db9b23c87340d" + integrity sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-visitor-keys@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" @@ -5223,6 +5465,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.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + eslint-webpack-plugin@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz#1978cdb9edc461e4b0195a20da950cf57988347c" @@ -5277,6 +5524,56 @@ eslint@^8.3.0: strip-ansi "^6.0.1" text-table "^0.2.0" +eslint@^9.24.0: + version "9.24.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.24.0.tgz#9a7f2e6cb2de81c405ab244b02f4584c79dc6bee" + integrity sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.20.0" + "@eslint/config-helpers" "^0.2.0" + "@eslint/core" "^0.12.0" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.24.0" + "@eslint/plugin-kit" "^0.2.7" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.3.0" + eslint-visitor-keys "^4.2.0" + espree "^10.3.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^10.0.1, espree@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.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" @@ -5303,6 +5600,13 @@ esquery@^1.4.2: dependencies: estraverse "^5.1.0" +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -5449,6 +5753,17 @@ fast-glob@^3.2.12, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -5487,6 +5802,13 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + file-loader@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" @@ -5514,6 +5836,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + finalhandler@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" @@ -5605,6 +5934,14 @@ flat-cache@^3.0.4: keyv "^4.5.3" rimraf "^3.0.2" +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + flatted@^3.2.9: version "3.2.9" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" @@ -5847,6 +6184,11 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + globalthis@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" @@ -6169,6 +6511,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +ignore@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + immer@^9.0.21, immer@^9.0.7: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" @@ -7276,7 +7623,7 @@ jsonpointer@^5.0.0: object.assign "^4.1.4" object.values "^1.1.6" -keyv@^4.5.3: +keyv@^4.5.3, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -7855,6 +8202,14 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -7908,6 +8263,13 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -7930,7 +8292,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -9744,6 +10106,11 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3: dependencies: lru-cache "^6.0.0" +semver@^7.6.0: + version "7.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" + integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -10466,6 +10833,11 @@ tryer@^1.0.1: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== +ts-api-utils@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" @@ -10591,6 +10963,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@^5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + typo-js@*: version "1.2.4" resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.2.4.tgz#0e009c289a966dd51dc80a75580289a381cc607f" @@ -10621,6 +10998,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"