diff --git a/frontend/.github/workflows/playwright.yml b/frontend/.github/workflows/playwright.yml new file mode 100644 index 0000000..8116248 --- /dev/null +++ b/frontend/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm exec playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/frontend/.gitignore b/frontend/.gitignore index 9a0f74f..09f7ef7 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -28,3 +28,7 @@ dist-ssr .env.*.local .env.*.production *.local +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/frontend/index.html b/frontend/index.html index 9d66414..97bf58d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -25,20 +25,14 @@ crossorigin="anonymous" /> - diff --git a/frontend/package.json b/frontend/package.json index 90e5696..e20c4c3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "test": "vitest" + "test": "vitest --watch" }, "dependencies": { "@betting-duck/shared": "workspace:*", @@ -21,7 +21,7 @@ "@radix-ui/react-slot": "^1.1.0", "@react-three/cannon": "^6.6.0", "@react-three/drei": "^9.119.0", - "@react-three/fiber": "^8.17.10", + "@react-three/fiber": "^8.17.12", "@tanstack/react-query": "^5.62.0", "@tanstack/react-router": "^1.81.1", "@types/three": "^0.170.0", @@ -30,33 +30,44 @@ "framer-motion": "^11.12.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "recoil": "^0.7.7", "socket.io-client": "^4.8.1", "tailwind-merge": "^2.5.4", - "three": "^0.171.0" + "terser": "^5.37.0", + "three": "^0.172.0" }, "devDependencies": { "@eslint/js": "^9.13.0", + "@playwright/test": "^1.49.1", "@tanstack/router-cli": "^1.79.0", - "@tanstack/router-devtools": "^1.81.1", + "@tanstack/router-devtools": "^1.81.5", "@tanstack/router-plugin": "^1.79.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.2.0", "@types/node": "^22.9.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", "autoprefixer": "^10.4.20", + "cssnano": "^7.0.6", "eslint": "^9.13.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", + "jsdom": "^26.0.0", "msw": "^2.6.4", "postcss": "^8.4.48", "prettier": "^3.3.3", + "rollup-plugin-visualizer": "^5.14.0", + "sharp": "^0.33.5", "tailwindcss": "^3.4.14", "typescript": "~5.6.3", "typescript-eslint": "^8.11.0", "vite": "^5.4.10", + "vite-plugin-compression2": "^1.3.3", + "vite-plugin-image-optimizer": "^1.1.8", "vite-tsconfig-paths": "^5.1.3", - "vitest": "^2.1.4" + "vitest": "^2.1.5" }, "msw": { "workerDirectory": [ diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..6d42e1b --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://127.0.0.1:3000", + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + + // webServer: { + // command: "pnpm run preview", + // url: "http://localhost:4173", + // reuseExistingServer: !process.env.CI, + // } +}); diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 2e7af2b..3fc77ed 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -2,5 +2,6 @@ export default { plugins: { tailwindcss: {}, autoprefixer: {}, + ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}), }, -} +}; diff --git a/frontend/setupTests.ts b/frontend/setupTests.ts new file mode 100644 index 0000000..26ff893 --- /dev/null +++ b/frontend/setupTests.ts @@ -0,0 +1,2 @@ +// vitest.setup.ts +import "@testing-library/react"; diff --git a/frontend/src/app/layout/index.tsx b/frontend/src/app/layout/index.tsx new file mode 100644 index 0000000..d35e90e --- /dev/null +++ b/frontend/src/app/layout/index.tsx @@ -0,0 +1 @@ +export { Layout } from "./ui/Layout"; diff --git a/frontend/src/app/layout/ui/Layout.tsx b/frontend/src/app/layout/ui/Layout.tsx new file mode 100644 index 0000000..cb8fd17 --- /dev/null +++ b/frontend/src/app/layout/ui/Layout.tsx @@ -0,0 +1,37 @@ +import { useLayout } from "@/shared/hooks/useLayout"; +import { cn } from "@/shared/misc"; +import { RootHeader } from "./RootHeader"; +import { RootSideBar } from "./RootSidebar"; +import { Suspense } from "react"; +import { LoadingAnimation } from "@/shared/components/Loading"; +import { useRecoilValue } from "recoil"; +import { Auth } from "@/app/provider/RouterProvider/lib/auth"; + +const layoutStyles = { + default: "max-w-[520px]", + wide: "max-w-[1200px]", +} as const; + +function Layout({ children }: { children: React.ReactNode }) { + const { layoutType } = useLayout(); + const authData = useRecoilValue(Auth); + + return ( +
+ }> + + + + {children} +
+ ); +} + +export { Layout }; diff --git a/frontend/src/app/layout/ui/NavItem.tsx b/frontend/src/app/layout/ui/NavItem.tsx new file mode 100644 index 0000000..4027a3c --- /dev/null +++ b/frontend/src/app/layout/ui/NavItem.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { createLink, LinkComponent, useLocation } from "@tanstack/react-router"; + +interface LinkComponentProps + extends React.AnchorHTMLAttributes { + icon: React.ReactNode; + label: string; +} + +const Link = React.forwardRef( + ({ icon, label, className, ...props }, ref) => { + const location = useLocation(); + const isChecked = location.pathname.includes(label.split(" ")[0]); + + return ( + + + + ); + }, +); + +const CreatedLinkComponent = createLink(Link); + +const NavItem: LinkComponent = (props) => { + return ( + + ); +}; + +export { NavItem }; diff --git a/frontend/src/app/layout/ui/RootHeader.tsx b/frontend/src/app/layout/ui/RootHeader.tsx new file mode 100644 index 0000000..5a1d636 --- /dev/null +++ b/frontend/src/app/layout/ui/RootHeader.tsx @@ -0,0 +1,37 @@ +import { LogoIcon } from "@/shared/icons"; +import { Image } from "@/shared/components/Image"; +import waitingUserImage from "@assets/images/waiting-user.avif"; +import { Link } from "@tanstack/react-router"; + +function UserInfo({ nickname }: { nickname: string }) { + return ( +
+
+ 대기 중인 사용자 이미지 +
+ {nickname} +
+ ); +} + +function RootHeader({ nickname }: { nickname: string }) { + return ( +
+ + +

Betting Duck

+ + {nickname ? : null} +
+ ); +} + +export { RootHeader }; diff --git a/frontend/src/app/layout/ui/RootSidebar.module.css b/frontend/src/app/layout/ui/RootSidebar.module.css new file mode 100644 index 0000000..7b6e25e --- /dev/null +++ b/frontend/src/app/layout/ui/RootSidebar.module.css @@ -0,0 +1,15 @@ +.navigator { + content: ""; + position: absolute; + + top: 0; + left: 0; + + width: 4px; + height: 46px; + + background: #6e29da; + transition: transform 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + + transform: translateY(calc(48px * calc(1.5 * var(--navigator-position)))); +} diff --git a/frontend/src/app/layout/ui/RootSidebar.tsx b/frontend/src/app/layout/ui/RootSidebar.tsx new file mode 100644 index 0000000..9cc6efd --- /dev/null +++ b/frontend/src/app/layout/ui/RootSidebar.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { + LoginIcon, + UserIcon, + CreateVoteIcon, + WaitingRoomIcon, +} from "@/shared/icons"; +import { NavItem } from "./NavItem"; +import styles from "./RootSidebar.module.css"; +import { useLocation } from "@tanstack/react-router"; +import { LogoutButton } from "@/pages/login-page/ui/components/Logout"; + +type NavItemType = { + id: string; + icon: () => JSX.Element; + label: string; + href: string; + onClick?: () => void; +}; + +const navItems = { + top: [ + { icon: UserIcon, id: "my-page-navigation", label: "my", href: "/my-page" }, + { + icon: CreateVoteIcon, + id: "create-vote-page-navigation", + label: "create vote", + href: "/create-vote", + }, + { + icon: WaitingRoomIcon, + id: "betting-room-navigation", + label: "betting", + href: "/betting", + }, + ], + login: [ + { + icon: LoginIcon, + id: "login-page-navigation", + label: "login", + href: "/login", + }, + ], +}; + +function changeNavigatorPosition(href: string) { + let nextPosition = 0; + if (href.includes("my-page") || href.includes("guest-login")) { + nextPosition = 0; + } else if (href.includes("create-vote")) { + nextPosition = 1; + } else if (href.includes("betting") || href.includes("vote")) { + nextPosition = 2.2; + } else if (href.includes("login")) { + nextPosition = 9.9; + } + + document.documentElement.style.setProperty( + "--navigator-position", + nextPosition.toString(), + ); +} + +function NavItems({ items }: { items: NavItemType[] }) { + return ( + + ); +} + +function RootSideBar({ isAuthenticated }: { isAuthenticated: boolean }) { + const location = useLocation(); + + React.useEffect(() => { + changeNavigatorPosition(location.href); + }, [location]); + + return ( + + ); +} + +export { RootSideBar }; diff --git a/frontend/src/app/provider/AuthProvider/AuthProvider.tsx b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx new file mode 100644 index 0000000..0a93065 --- /dev/null +++ b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx @@ -0,0 +1,37 @@ +import { useEffect } from "react"; +import { Fragment } from "react/jsx-runtime"; +import { useSetRecoilState } from "recoil"; +import { Auth } from "@/app/provider/RouterProvider/lib/auth"; +import { useQuery } from "@tanstack/react-query"; +import { authQueries } from "@/shared/lib/auth/authQuery"; + +function AuthProvider({ children }: { children: React.ReactNode }) { + const setAuthState = useSetRecoilState(Auth); + const { data, isLoading } = useQuery({ + queryKey: authQueries.queryKey, + queryFn: authQueries.queryFn, + }); + + useEffect(() => { + if (!isLoading) { + if (data?.isAuthenticated) { + setAuthState((prev) => ({ + ...prev, + isLoading: false, + isAuthenticated: data.isAuthenticated, + nickname: data.userInfo.nickname, + })); + } else { + setAuthState((prev) => ({ + ...prev, + isLoading: false, + isAuthenticated: false, + })); + } + } + }, [data, isLoading, setAuthState]); + + return {children}; +} + +export { AuthProvider }; diff --git a/frontend/src/app/provider/AuthProvider/index.ts b/frontend/src/app/provider/AuthProvider/index.ts new file mode 100644 index 0000000..842d0c1 --- /dev/null +++ b/frontend/src/app/provider/AuthProvider/index.ts @@ -0,0 +1 @@ +export { AuthProvider } from "./AuthProvider"; diff --git a/frontend/src/app/provider/RouterProvider/RouterProvider.tsx b/frontend/src/app/provider/RouterProvider/RouterProvider.tsx new file mode 100644 index 0000000..7d05e2d --- /dev/null +++ b/frontend/src/app/provider/RouterProvider/RouterProvider.tsx @@ -0,0 +1,38 @@ +import { RouterProvider, createRouter } from "@tanstack/react-router"; +import { routeTree } from "@/routeTree.gen"; +import { Auth, AuthState } from "./lib/auth"; +import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; +import { QueryClient } from "@tanstack/react-query"; +import { RecoilState } from "recoil"; + +type RouterContext = { + auth: RecoilState; + queryClient: QueryClient; +}; + +const createGlobalRouter = (queryClient: QueryClient) => + createRouter({ + routeTree, + context: { + auth: Auth, + queryClient, + } as RouterContext, + defaultPreload: "intent", + defaultErrorComponent: ({ error }) => ( + + ), + }); + +function GlobalRouter({ queryClient }: { queryClient: QueryClient }) { + const router = createGlobalRouter(queryClient); + return ; +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} + +export { GlobalRouter }; +export type { RouterContext }; diff --git a/frontend/src/app/provider/RouterProvider/index.ts b/frontend/src/app/provider/RouterProvider/index.ts new file mode 100644 index 0000000..e3f55bb --- /dev/null +++ b/frontend/src/app/provider/RouterProvider/index.ts @@ -0,0 +1,2 @@ +export { GlobalRouter } from "./RouterProvider"; +export type { RouterContext } from "./RouterProvider"; diff --git a/frontend/src/app/provider/RouterProvider/lib/auth.ts b/frontend/src/app/provider/RouterProvider/lib/auth.ts new file mode 100644 index 0000000..17b3af6 --- /dev/null +++ b/frontend/src/app/provider/RouterProvider/lib/auth.ts @@ -0,0 +1,18 @@ +import { atom } from "recoil"; + +export type AuthState = { + isAuthenticated: boolean; + nickname?: string; + isLoading: boolean; + roomId?: string; +}; + +export const Auth = atom({ + key: "auth", + default: { + isAuthenticated: false, + isLoading: true, + nickname: undefined, + roomId: undefined, + }, +}); diff --git a/frontend/src/assets/images/main-logo.avif b/frontend/src/assets/images/main-logo.avif new file mode 100644 index 0000000..5a27e0f Binary files /dev/null and b/frontend/src/assets/images/main-logo.avif differ diff --git a/frontend/src/assets/images/main-logo.avif:Zone.Identifier b/frontend/src/assets/images/main-logo.avif:Zone.Identifier new file mode 100644 index 0000000..45a0c90 --- /dev/null +++ b/frontend/src/assets/images/main-logo.avif:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=https://squoosh.app/ diff --git a/frontend/src/assets/images/main-logo.png b/frontend/src/assets/images/main-logo.png deleted file mode 100644 index 07586a1..0000000 Binary files a/frontend/src/assets/images/main-logo.png and /dev/null differ diff --git a/frontend/src/assets/images/pond.png b/frontend/src/assets/images/pond.png deleted file mode 100644 index 9e39c6d..0000000 Binary files a/frontend/src/assets/images/pond.png and /dev/null differ diff --git a/frontend/src/assets/images/waiting-user.avif b/frontend/src/assets/images/waiting-user.avif new file mode 100644 index 0000000..803a529 Binary files /dev/null and b/frontend/src/assets/images/waiting-user.avif differ diff --git a/frontend/src/assets/images/waiting-user.avif:Zone.Identifier b/frontend/src/assets/images/waiting-user.avif:Zone.Identifier new file mode 100644 index 0000000..45a0c90 --- /dev/null +++ b/frontend/src/assets/images/waiting-user.avif:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=https://squoosh.app/ diff --git a/frontend/src/assets/images/waiting-user.png b/frontend/src/assets/images/waiting-user.png deleted file mode 100644 index 1250fa9..0000000 Binary files a/frontend/src/assets/images/waiting-user.png and /dev/null differ diff --git a/frontend/src/features/login-page/ui/components/GuestLoginForm.tsx b/frontend/src/features/login-page/ui/components/GuestLoginForm.tsx deleted file mode 100644 index 15173ff..0000000 --- a/frontend/src/features/login-page/ui/components/GuestLoginForm.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { LoginIDIcon } from "@/shared/icons"; -import { useEffect, useState } from "react"; -import { useAuthStore } from "../../model/store"; -import { Warning } from "./Warning"; -import { useNavigate } from "@tanstack/react-router"; -import { useUserContext } from "@/shared/hooks/useUserContext"; -import { useQueryClient } from "@tanstack/react-query"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import { z } from "zod"; -import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; - -function GuestLoginForm({ to, roomId }: { to?: string; roomId?: string }) { - const [nickname, setNickname] = useState(""); - const { error } = useAuthStore(); - const [isSignedUp, setIsSignedUp] = useState(false); - const queryClient = useQueryClient(); - - const { setUserInfo } = useUserContext(); - const navigate = useNavigate(); - - useEffect(() => { - const checkSignupStatus = async () => { - try { - const response = await fetch("/api/users/guestloginactivity", { - method: "GET", - }); - - if (!response.ok) { - throw new Error("회원가입 상태 확인에 실패했습니다."); - } - - const result = await response.json(); - const isSigned = result.data.loggedInBefore; - setIsSignedUp(isSigned); - if (isSigned) { - setNickname(result.data.nickname.replace(/^익명의 /, "")); - } - } catch (error) { - if (error instanceof Error) { - console.error("오류 발생:", error.message); - } else { - console.error("알 수 없는 오류 발생:", error); - } - } - }; - - checkSignupStatus(); - }); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - // const result = await handleGuestLogin({ nickname: `익명의 ${nickname}` }); - try { - const response = await fetch("/api/users/guestsignin", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ nickname: `익명의 ${nickname}` }), - }); - if (!response.ok) throw new Error("게스트 로그인에 실패했습니다."); - const { data } = await response.json(); - setUserInfo({ - role: "guest", - nickname: data.nickname, - isAuthenticated: true, - }); - - queryClient.setQueryData( - authQueries.queryKey, - (prev: z.infer) => ({ - ...prev, - userInfo: { - role: "guest", - nickname: data.nickname, - duck: 0, - message: "OK", - realDuck: prev.userInfo.realDuck, - }, - }), - ); - await queryClient.invalidateQueries({ queryKey: authQueries.queryKey }); - - if (to && roomId) { - window.location.href = `/betting/${roomId}/waiting`; - } - - navigate({ - to: "/my-page", - search: { - nickname: decodeURIComponent(nickname), - }, - }); - } catch (error) { - console.error("게스트 로그인에 실패했습니다.", error); - } - }; - return ( -
-
-
- -
-
- {isSignedUp && ( - - )} - {error && } - - - ); -} - -export { GuestLoginForm }; diff --git a/frontend/src/features/login-page/ui/components/Logout.tsx b/frontend/src/features/login-page/ui/components/Logout.tsx deleted file mode 100644 index 1a32659..0000000 --- a/frontend/src/features/login-page/ui/components/Logout.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useUserContext } from "@/shared/hooks/useUserContext"; -import { LogoutIcon } from "@/shared/icons/Logout"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; -import { useQueryClient } from "@tanstack/react-query"; -import { useNavigate } from "@tanstack/react-router"; -import { z } from "zod"; - -function logout() { - // 모든 가능한 조합으로 쿠키 삭제 시도 - const cookieSettings = [ - // 기본 설정 - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/", - // 모든 path 조합 - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/api", - // httpOnly가 아닌 경우를 위한 설정 - "access_token=; max-age=-99999999; path=/", - // 도메인 설정 - `access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${window.location.hostname}`, - // subdomain을 포함한 도메인 설정 - `access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname}`, - // Secure 옵션 포함 - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure", - // SameSite 옵션들 포함 - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=strict", - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=lax", - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=none", - ]; - - // 모든 설정 조합 적용 - cookieSettings.forEach((setting) => { - document.cookie = setting; - }); -} - -function LogoutButton() { - const navigate = useNavigate(); - const { setUserInfo } = useUserContext(); - const queryClient = useQueryClient(); - - return ( - - ); -} - -export { LogoutButton }; diff --git a/frontend/src/features/my-page/index.tsx b/frontend/src/features/my-page/index.tsx deleted file mode 100644 index 39a05f2..0000000 --- a/frontend/src/features/my-page/index.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { DuckCoinIcon } from "@/shared/icons"; -import { useNavigate } from "@tanstack/react-router"; -import { Pond } from "./ui/Pond"; -import { FallingDuck } from "./ui/FallingDuck"; -import React from "react"; -import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import { AnimatedDuckCount } from "./ui/AnimatedDuckCount"; -import { z } from "zod"; -import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; - -function MyPage() { - const { data: authData } = useSuspenseQuery({ - queryKey: authQueries.queryKey, - queryFn: authQueries.queryFn, - }); - const queryClient = useQueryClient(); - - const navigate = useNavigate(); - const [currentDuck, setCurrentDuck] = React.useState(authData.userInfo.duck); - const [numberOfDucks, setNumberOfDucks] = React.useState( - authData.userInfo.realDuck, - ); - const [ducks, setDucks] = React.useState([FallingDuck]); - - React.useEffect(() => { - const remainingDucks = numberOfDucks - ducks.length; - if (remainingDucks <= 0) return; - - const addDuck = (count: number) => { - if (count >= remainingDucks) return; - const timer = setTimeout(() => { - setDucks((prevDucks) => [...prevDucks, FallingDuck]); - addDuck(count + 1); // 다음 오리 추가 - }, 200); - return () => clearTimeout(timer); - }; - - const initialTimer = setTimeout(() => { - addDuck(0); - }, 2000); - return () => clearTimeout(initialTimer); - }, [numberOfDucks, ducks.length]); - - async function purchaseDuck() { - if (currentDuck < 30) { - return; - } - - const response = await fetch("/api/users/purchaseduck"); - if (!response.ok) { - console.error("Failed to purchase duck"); - return; - } - await queryClient.setQueryData( - authQueries.queryKey, - (old: z.infer) => ({ - ...old, - userInfo: { - ...old.userInfo, - duck: currentDuck - 30, - realDuck: numberOfDucks + 1, - }, - }), - ); - setDucks([...ducks, FallingDuck]); - setCurrentDuck(currentDuck - 30); - setNumberOfDucks(numberOfDucks + 1); - } - - return ( -
-
-
-

마이 페이지

-

오리를 구매해서 페이지를 꾸며보세요

-
- - -
- -
-
-
- - -
-
-
- ); -} - -export { MyPage }; diff --git a/frontend/src/features/my-page/ui/FallingDuck.tsx b/frontend/src/features/my-page/ui/FallingDuck.tsx deleted file mode 100644 index a1315a7..0000000 --- a/frontend/src/features/my-page/ui/FallingDuck.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as THREE from "three"; -import { useBox } from "@react-three/cannon"; -import { Gltf } from "@react-three/drei"; -import duckModel from "@/assets/models/betting-duck.glb"; - -function FallingDuck() { - // 약간의 랜덤 오프셋 추가 (x축과 z축에 대해) - const randomX = (Math.random() - 0.5) * 2; // -1 ~ 1 사이의 랜덤값 - const randomZ = (Math.random() - 0.5) * 2; // -1 ~ 1 사이의 랜덤값 - - const [ref] = useBox(() => ({ - mass: 1, - position: [randomX, 5, randomZ], // 초기 위치에 랜덤값 적용 - rotation: [ - Math.random() * 0.2, // 약간의 랜덤 회전도 추가 - Math.random() * 0.2, - Math.random() * 0.2, - ], - linearDamping: 0.4, - angularDamping: 0.4, - material: { - friction: 0.3, - restitution: 0.3, - }, - })); - - return ( - } - castShadow - receiveShadow - src={duckModel} - /> - ); -} - -export { FallingDuck }; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 6da5725..d6b69e3 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,12 +1,11 @@ import "./index.css"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { RouterProvider, createRouter } from "@tanstack/react-router"; -import { routeTree } from "./routeTree.gen"; -import { Auth } from "@/shared/lib/auth/auth"; import { ThemeProvider, createTheme } from "@mui/material/styles"; -import { GlobalErrorComponent } from "./shared/components/Error/GlobalError"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { GlobalRouter } from "./app/provider/RouterProvider"; +import { RecoilRoot } from "recoil"; +import { AuthProvider } from "./app/provider/AuthProvider/AuthProvider"; const queryClient = new QueryClient({ defaultOptions: { @@ -17,39 +16,18 @@ const queryClient = new QueryClient({ }, }); -type RouterContext = { - auth: Auth; - queryClient: QueryClient; -}; - -const router = createRouter({ - routeTree, - context: { - auth: {} as Auth, - queryClient, - } as RouterContext, - defaultPreload: "intent", - defaultErrorComponent: ({ error }) => ( - - ), -}); - -declare module "@tanstack/react-router" { - interface Register { - router: typeof router; - } -} - const theme = createTheme(); createRoot(document.getElementById("root")!).render( - - - + + + + + + + , ); - -export { type RouterContext }; diff --git a/frontend/src/features/betting-page-admin/EndPredictButton.tsx b/frontend/src/pages/betting-page-admin/EndPredictButton.tsx similarity index 100% rename from frontend/src/features/betting-page-admin/EndPredictButton.tsx rename to frontend/src/pages/betting-page-admin/EndPredictButton.tsx diff --git a/frontend/src/features/betting-page-admin/index.tsx b/frontend/src/pages/betting-page-admin/index.tsx similarity index 100% rename from frontend/src/features/betting-page-admin/index.tsx rename to frontend/src/pages/betting-page-admin/index.tsx diff --git a/frontend/src/features/betting-page-admin/model/api.ts b/frontend/src/pages/betting-page-admin/model/api.ts similarity index 100% rename from frontend/src/features/betting-page-admin/model/api.ts rename to frontend/src/pages/betting-page-admin/model/api.ts diff --git a/frontend/src/features/betting-page-admin/model/types.ts b/frontend/src/pages/betting-page-admin/model/types.ts similarity index 100% rename from frontend/src/features/betting-page-admin/model/types.ts rename to frontend/src/pages/betting-page-admin/model/types.ts diff --git a/frontend/src/features/betting-page-admin/ui/BettingDetails.tsx b/frontend/src/pages/betting-page-admin/ui/BettingDetails.tsx similarity index 100% rename from frontend/src/features/betting-page-admin/ui/BettingDetails.tsx rename to frontend/src/pages/betting-page-admin/ui/BettingDetails.tsx diff --git a/frontend/src/features/betting-page/api/endBetroom.ts b/frontend/src/pages/betting-page/api/endBetroom.ts similarity index 100% rename from frontend/src/features/betting-page/api/endBetroom.ts rename to frontend/src/pages/betting-page/api/endBetroom.ts diff --git a/frontend/src/features/betting-page/api/getBettingRoomInfo.ts b/frontend/src/pages/betting-page/api/getBettingRoomInfo.ts similarity index 73% rename from frontend/src/features/betting-page/api/getBettingRoomInfo.ts rename to frontend/src/pages/betting-page/api/getBettingRoomInfo.ts index 2181a2e..fbabf86 100644 --- a/frontend/src/features/betting-page/api/getBettingRoomInfo.ts +++ b/frontend/src/pages/betting-page/api/getBettingRoomInfo.ts @@ -1,16 +1,17 @@ import { responseBetRoomInfo } from "@betting-duck/shared"; async function getBettingRoomInfo(roomId: string) { + if (!roomId) return null; try { const response = await fetch(`/api/betrooms/${roomId}`); if (!response.ok) { - return null; // 에러 대신 null 반환 + return null; } const { data } = await response.json(); const result = responseBetRoomInfo.safeParse(data); if (!result.success) { - return null; // 파싱 실패시에도 null 반환 + return null; } return { ...result.data, @@ -18,7 +19,7 @@ async function getBettingRoomInfo(roomId: string) { placeBetAmount: 0, }; } catch { - return null; // 네트워크 에러 등의 경우에도 null 반환 + return null; } } diff --git a/frontend/src/features/betting-page/api/getUserInfo.ts b/frontend/src/pages/betting-page/api/getUserInfo.ts similarity index 100% rename from frontend/src/features/betting-page/api/getUserInfo.ts rename to frontend/src/pages/betting-page/api/getUserInfo.ts diff --git a/frontend/src/features/betting-page/hook/useBettingContext.ts b/frontend/src/pages/betting-page/hook/useBettingContext.ts similarity index 100% rename from frontend/src/features/betting-page/hook/useBettingContext.ts rename to frontend/src/pages/betting-page/hook/useBettingContext.ts diff --git a/frontend/src/features/betting-page/hook/useBettingRoomConnection.ts b/frontend/src/pages/betting-page/hook/useBettingRoomConnection.ts similarity index 100% rename from frontend/src/features/betting-page/hook/useBettingRoomConnection.ts rename to frontend/src/pages/betting-page/hook/useBettingRoomConnection.ts diff --git a/frontend/src/features/betting-page/index.tsx b/frontend/src/pages/betting-page/index.tsx similarity index 100% rename from frontend/src/features/betting-page/index.tsx rename to frontend/src/pages/betting-page/index.tsx diff --git a/frontend/src/features/betting-page/model/schema.ts b/frontend/src/pages/betting-page/model/schema.ts similarity index 100% rename from frontend/src/features/betting-page/model/schema.ts rename to frontend/src/pages/betting-page/model/schema.ts diff --git a/frontend/src/features/betting-page/model/var.ts b/frontend/src/pages/betting-page/model/var.ts similarity index 100% rename from frontend/src/features/betting-page/model/var.ts rename to frontend/src/pages/betting-page/model/var.ts diff --git a/frontend/src/features/betting-page/provider/BettingProvider.tsx b/frontend/src/pages/betting-page/provider/BettingProvider.tsx similarity index 100% rename from frontend/src/features/betting-page/provider/BettingProvider.tsx rename to frontend/src/pages/betting-page/provider/BettingProvider.tsx diff --git a/frontend/src/features/betting-page/ui/BettingContainer.tsx b/frontend/src/pages/betting-page/ui/BettingContainer.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/BettingContainer.tsx rename to frontend/src/pages/betting-page/ui/BettingContainer.tsx diff --git a/frontend/src/features/betting-page/ui/BettingFooter.tsx b/frontend/src/pages/betting-page/ui/BettingFooter.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/BettingFooter.tsx rename to frontend/src/pages/betting-page/ui/BettingFooter.tsx diff --git a/frontend/src/features/betting-page/ui/BettingHeader.tsx b/frontend/src/pages/betting-page/ui/BettingHeader.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/BettingHeader.tsx rename to frontend/src/pages/betting-page/ui/BettingHeader.tsx diff --git a/frontend/src/features/betting-page/ui/BettingInput/index.tsx b/frontend/src/pages/betting-page/ui/BettingInput/index.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/BettingInput/index.tsx rename to frontend/src/pages/betting-page/ui/BettingInput/index.tsx diff --git a/frontend/src/features/betting-page/ui/TotalBettingDisplay.tsx b/frontend/src/pages/betting-page/ui/TotalBettingDisplay.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/TotalBettingDisplay.tsx rename to frontend/src/pages/betting-page/ui/TotalBettingDisplay.tsx diff --git a/frontend/src/features/betting-page/utils/placeBetting.ts b/frontend/src/pages/betting-page/utils/placeBetting.ts similarity index 100% rename from frontend/src/features/betting-page/utils/placeBetting.ts rename to frontend/src/pages/betting-page/utils/placeBetting.ts diff --git a/frontend/src/features/betting-predict-result/index.tsx b/frontend/src/pages/betting-predict-result/index.tsx similarity index 100% rename from frontend/src/features/betting-predict-result/index.tsx rename to frontend/src/pages/betting-predict-result/index.tsx diff --git a/frontend/src/features/chat/hook/useChat.tsx b/frontend/src/pages/chat/hook/useChat.tsx similarity index 100% rename from frontend/src/features/chat/hook/useChat.tsx rename to frontend/src/pages/chat/hook/useChat.tsx diff --git a/frontend/src/features/chat/index.tsx b/frontend/src/pages/chat/index.tsx similarity index 91% rename from frontend/src/features/chat/index.tsx rename to frontend/src/pages/chat/index.tsx index fea9158..17c4546 100644 --- a/frontend/src/features/chat/index.tsx +++ b/frontend/src/pages/chat/index.tsx @@ -1,6 +1,6 @@ -import { ChatHeader } from "@/features/chat/ui/ChatHeader"; -import { ChatInput } from "@/features/chat/ui/ChatInput"; -import { ChatMessages } from "@/features/chat/ui/ChatMessages"; +import { ChatHeader } from "@/pages/chat/ui/ChatHeader"; +import { ChatInput } from "@/pages/chat/ui/ChatInput"; +import { ChatMessages } from "@/pages/chat/ui/ChatMessages"; import { ChatProvider } from "./provider/ChatProvider"; import { cn } from "@/shared/misc"; import { useLoaderData } from "@tanstack/react-router"; diff --git a/frontend/src/features/chat/provider/ChatProvider.tsx b/frontend/src/pages/chat/provider/ChatProvider.tsx similarity index 100% rename from frontend/src/features/chat/provider/ChatProvider.tsx rename to frontend/src/pages/chat/provider/ChatProvider.tsx diff --git a/frontend/src/features/chat/ui/ChatError/index.tsx b/frontend/src/pages/chat/ui/ChatError/index.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatError/index.tsx rename to frontend/src/pages/chat/ui/ChatError/index.tsx diff --git a/frontend/src/features/chat/ui/ChatHeader/index.tsx b/frontend/src/pages/chat/ui/ChatHeader/index.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatHeader/index.tsx rename to frontend/src/pages/chat/ui/ChatHeader/index.tsx diff --git a/frontend/src/features/chat/ui/ChatHeader/ui/ChatTitle.tsx b/frontend/src/pages/chat/ui/ChatHeader/ui/ChatTitle.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatHeader/ui/ChatTitle.tsx rename to frontend/src/pages/chat/ui/ChatHeader/ui/ChatTitle.tsx diff --git a/frontend/src/features/chat/ui/ChatHeader/ui/PredictButton.tsx b/frontend/src/pages/chat/ui/ChatHeader/ui/PredictButton.tsx similarity index 85% rename from frontend/src/features/chat/ui/ChatHeader/ui/PredictButton.tsx rename to frontend/src/pages/chat/ui/ChatHeader/ui/PredictButton.tsx index e7fb31a..4c732da 100644 --- a/frontend/src/features/chat/ui/ChatHeader/ui/PredictButton.tsx +++ b/frontend/src/pages/chat/ui/ChatHeader/ui/PredictButton.tsx @@ -1,12 +1,12 @@ -import { BettingPage } from "@/features/betting-page"; -import { useChat } from "@/features/chat/hook/useChat"; +import { BettingPage } from "@/pages/betting-page"; +import { useChat } from "@/pages/chat/hook/useChat"; import { Dialog, DialogTrigger, DialogContent, } from "@/shared/components/Dialog"; import { useNavigate, useParams } from "@tanstack/react-router"; -import { STORAGE_KEY } from "@/features/betting-page/model/var"; +import { STORAGE_KEY } from "@/pages/betting-page/model/var"; import { useUserContext } from "@/shared/hooks/useUserContext"; function PredictButton() { diff --git a/frontend/src/features/chat/ui/ChatHeader/ui/PredictionStatus.tsx b/frontend/src/pages/chat/ui/ChatHeader/ui/PredictionStatus.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatHeader/ui/PredictionStatus.tsx rename to frontend/src/pages/chat/ui/ChatHeader/ui/PredictionStatus.tsx diff --git a/frontend/src/features/chat/ui/ChatInput/index.tsx b/frontend/src/pages/chat/ui/ChatInput/index.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatInput/index.tsx rename to frontend/src/pages/chat/ui/ChatInput/index.tsx diff --git a/frontend/src/features/chat/ui/ChatInput/ui/EmoticonButton.tsx b/frontend/src/pages/chat/ui/ChatInput/ui/EmoticonButton.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatInput/ui/EmoticonButton.tsx rename to frontend/src/pages/chat/ui/ChatInput/ui/EmoticonButton.tsx diff --git a/frontend/src/features/chat/ui/ChatInput/ui/InputBar.tsx b/frontend/src/pages/chat/ui/ChatInput/ui/InputBar.tsx similarity index 98% rename from frontend/src/features/chat/ui/ChatInput/ui/InputBar.tsx rename to frontend/src/pages/chat/ui/ChatInput/ui/InputBar.tsx index 0692eb9..e4de810 100644 --- a/frontend/src/features/chat/ui/ChatInput/ui/InputBar.tsx +++ b/frontend/src/pages/chat/ui/ChatInput/ui/InputBar.tsx @@ -1,5 +1,5 @@ import { UserInfo } from "@/app/provider/UserProvider"; -import { useChat } from "@/features/chat/hook/useChat"; +import { useChat } from "@/pages/chat/hook/useChat"; import { useParams } from "@tanstack/react-router"; import React from "react"; diff --git a/frontend/src/features/chat/ui/ChatInput/ui/VoteButton.tsx b/frontend/src/pages/chat/ui/ChatInput/ui/VoteButton.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatInput/ui/VoteButton.tsx rename to frontend/src/pages/chat/ui/ChatInput/ui/VoteButton.tsx diff --git a/frontend/src/features/chat/ui/ChatInput/ui/style.module.css b/frontend/src/pages/chat/ui/ChatInput/ui/style.module.css similarity index 100% rename from frontend/src/features/chat/ui/ChatInput/ui/style.module.css rename to frontend/src/pages/chat/ui/ChatInput/ui/style.module.css diff --git a/frontend/src/features/chat/ui/ChatMessages/index.tsx b/frontend/src/pages/chat/ui/ChatMessages/index.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatMessages/index.tsx rename to frontend/src/pages/chat/ui/ChatMessages/index.tsx diff --git a/frontend/src/features/chat/ui/ChatMessages/ui/Message.tsx b/frontend/src/pages/chat/ui/ChatMessages/ui/Message.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatMessages/ui/Message.tsx rename to frontend/src/pages/chat/ui/ChatMessages/ui/Message.tsx diff --git a/frontend/src/features/chat/ui/ChatMessages/ui/MessageList.tsx b/frontend/src/pages/chat/ui/ChatMessages/ui/MessageList.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatMessages/ui/MessageList.tsx rename to frontend/src/pages/chat/ui/ChatMessages/ui/MessageList.tsx diff --git a/frontend/src/features/create-vote/index.ts b/frontend/src/pages/create-vote/index.ts similarity index 100% rename from frontend/src/features/create-vote/index.ts rename to frontend/src/pages/create-vote/index.ts diff --git a/frontend/src/features/create-vote/model/api.ts b/frontend/src/pages/create-vote/model/api.ts similarity index 100% rename from frontend/src/features/create-vote/model/api.ts rename to frontend/src/pages/create-vote/model/api.ts diff --git a/frontend/src/features/create-vote/model/helpers/formatData.ts b/frontend/src/pages/create-vote/model/helpers/formatData.ts similarity index 100% rename from frontend/src/features/create-vote/model/helpers/formatData.ts rename to frontend/src/pages/create-vote/model/helpers/formatData.ts diff --git a/frontend/src/features/create-vote/model/store.ts b/frontend/src/pages/create-vote/model/store.ts similarity index 100% rename from frontend/src/features/create-vote/model/store.ts rename to frontend/src/pages/create-vote/model/store.ts diff --git a/frontend/src/features/create-vote/model/types.ts b/frontend/src/pages/create-vote/model/types.ts similarity index 100% rename from frontend/src/features/create-vote/model/types.ts rename to frontend/src/pages/create-vote/model/types.ts diff --git a/frontend/src/features/create-vote/model/useCaseInput.ts b/frontend/src/pages/create-vote/model/useCaseInput.ts similarity index 100% rename from frontend/src/features/create-vote/model/useCaseInput.ts rename to frontend/src/pages/create-vote/model/useCaseInput.ts diff --git a/frontend/src/features/create-vote/model/useCoinInput.ts b/frontend/src/pages/create-vote/model/useCoinInput.ts similarity index 100% rename from frontend/src/features/create-vote/model/useCoinInput.ts rename to frontend/src/pages/create-vote/model/useCoinInput.ts diff --git a/frontend/src/features/create-vote/model/useTimer.ts b/frontend/src/pages/create-vote/model/useTimer.ts similarity index 100% rename from frontend/src/features/create-vote/model/useTimer.ts rename to frontend/src/pages/create-vote/model/useTimer.ts diff --git a/frontend/src/features/create-vote/model/useTitleInput.ts b/frontend/src/pages/create-vote/model/useTitleInput.ts similarity index 100% rename from frontend/src/features/create-vote/model/useTitleInput.ts rename to frontend/src/pages/create-vote/model/useTitleInput.ts diff --git a/frontend/src/features/create-vote/model/useValidation.ts b/frontend/src/pages/create-vote/model/useValidation.ts similarity index 100% rename from frontend/src/features/create-vote/model/useValidation.ts rename to frontend/src/pages/create-vote/model/useValidation.ts diff --git a/frontend/src/features/create-vote/ui/CreateVotePage.tsx b/frontend/src/pages/create-vote/ui/CreateVotePage.tsx similarity index 100% rename from frontend/src/features/create-vote/ui/CreateVotePage.tsx rename to frontend/src/pages/create-vote/ui/CreateVotePage.tsx diff --git a/frontend/src/features/create-vote/ui/components/CaseInputs.tsx b/frontend/src/pages/create-vote/ui/components/CaseInputs.tsx similarity index 94% rename from frontend/src/features/create-vote/ui/components/CaseInputs.tsx rename to frontend/src/pages/create-vote/ui/components/CaseInputs.tsx index f40ebd8..7475ff8 100644 --- a/frontend/src/features/create-vote/ui/components/CaseInputs.tsx +++ b/frontend/src/pages/create-vote/ui/components/CaseInputs.tsx @@ -1,7 +1,7 @@ import React from "react"; import { InputField } from "../../../../shared/components/input/InputField"; import { DuckIcon } from "@/shared/icons"; -import { useCaseInput } from "@/features/create-vote/model/useCaseInput"; +import { useCaseInput } from "@/pages/create-vote/model/useCaseInput"; const CaseInputs = React.memo( ({ diff --git a/frontend/src/features/create-vote/ui/components/CoinInput.tsx b/frontend/src/pages/create-vote/ui/components/CoinInput.tsx similarity index 94% rename from frontend/src/features/create-vote/ui/components/CoinInput.tsx rename to frontend/src/pages/create-vote/ui/components/CoinInput.tsx index d124887..f1d8c8c 100644 --- a/frontend/src/features/create-vote/ui/components/CoinInput.tsx +++ b/frontend/src/pages/create-vote/ui/components/CoinInput.tsx @@ -1,6 +1,6 @@ import { ArrowDownIcon, ArrowUpIcon, DuckCoinIcon } from "@/shared/icons"; import React from "react"; -import { useCoinInput } from "@/features/create-vote/model/useCoinInput"; +import { useCoinInput } from "@/pages/create-vote/model/useCoinInput"; const CoinInput = React.memo( ({ initialValue = 100 }: { initialValue?: number }) => { diff --git a/frontend/src/features/create-vote/ui/components/Timer.tsx b/frontend/src/pages/create-vote/ui/components/Timer.tsx similarity index 97% rename from frontend/src/features/create-vote/ui/components/Timer.tsx rename to frontend/src/pages/create-vote/ui/components/Timer.tsx index a9a8470..fb45cbd 100644 --- a/frontend/src/features/create-vote/ui/components/Timer.tsx +++ b/frontend/src/pages/create-vote/ui/components/Timer.tsx @@ -1,6 +1,6 @@ import { ArrowDownIcon, ArrowUpIcon, TimerIcon } from "@/shared/icons"; import React from "react"; -import { useTimer } from "@/features/create-vote/model/useTimer"; +import { useTimer } from "@/pages/create-vote/model/useTimer"; const Timer = React.memo(({ initialValue = 1 }: { initialValue?: number }) => { const { value, incrementValue, decrementValue } = useTimer(initialValue); diff --git a/frontend/src/features/create-vote/ui/components/TitleInput.tsx b/frontend/src/pages/create-vote/ui/components/TitleInput.tsx similarity index 90% rename from frontend/src/features/create-vote/ui/components/TitleInput.tsx rename to frontend/src/pages/create-vote/ui/components/TitleInput.tsx index d215c26..c2b34eb 100644 --- a/frontend/src/features/create-vote/ui/components/TitleInput.tsx +++ b/frontend/src/pages/create-vote/ui/components/TitleInput.tsx @@ -1,7 +1,7 @@ import React from "react"; import { InputField } from "../../../../shared/components/input/InputField"; import { TextIcon } from "@/shared/icons"; -import { useTitleInput } from "@/features/create-vote/model/useTitleInput"; +import { useTitleInput } from "@/pages/create-vote/model/useTitleInput"; const TitleInput = React.memo( ({ initialValue = "" }: { initialValue?: string }) => { diff --git a/frontend/src/features/create-vote/ui/components/index.ts b/frontend/src/pages/create-vote/ui/components/index.ts similarity index 100% rename from frontend/src/features/create-vote/ui/components/index.ts rename to frontend/src/pages/create-vote/ui/components/index.ts diff --git a/frontend/src/features/create-vote/ui/error/CreateVoteError.tsx b/frontend/src/pages/create-vote/ui/error/CreateVoteError.tsx similarity index 100% rename from frontend/src/features/create-vote/ui/error/CreateVoteError.tsx rename to frontend/src/pages/create-vote/ui/error/CreateVoteError.tsx diff --git a/frontend/src/features/login-page/index.ts b/frontend/src/pages/login-page/index.ts similarity index 100% rename from frontend/src/features/login-page/index.ts rename to frontend/src/pages/login-page/index.ts diff --git a/frontend/src/features/login-page/model/api.ts b/frontend/src/pages/login-page/model/api.ts similarity index 100% rename from frontend/src/features/login-page/model/api.ts rename to frontend/src/pages/login-page/model/api.ts diff --git a/frontend/src/features/login-page/model/store.ts b/frontend/src/pages/login-page/model/store.ts similarity index 100% rename from frontend/src/features/login-page/model/store.ts rename to frontend/src/pages/login-page/model/store.ts diff --git a/frontend/src/features/login-page/model/types.ts b/frontend/src/pages/login-page/model/types.ts similarity index 100% rename from frontend/src/features/login-page/model/types.ts rename to frontend/src/pages/login-page/model/types.ts diff --git a/frontend/src/features/login-page/model/validation.ts b/frontend/src/pages/login-page/model/validation.ts similarity index 100% rename from frontend/src/features/login-page/model/validation.ts rename to frontend/src/pages/login-page/model/validation.ts diff --git a/frontend/src/features/login-page/ui/LoginPage.tsx b/frontend/src/pages/login-page/ui/LoginPage.tsx similarity index 98% rename from frontend/src/features/login-page/ui/LoginPage.tsx rename to frontend/src/pages/login-page/ui/LoginPage.tsx index 35f92ea..5807cd1 100644 --- a/frontend/src/features/login-page/ui/LoginPage.tsx +++ b/frontend/src/pages/login-page/ui/LoginPage.tsx @@ -1,6 +1,6 @@ import { cn } from "@/shared/misc"; import React, { useState } from "react"; -import mainLogo from "@/assets/images/main-logo.png"; +import mainLogo from "@/assets/images/main-logo.avif"; import { GuestLoginForm, LoginForm, diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/checkSignupStatus.ts b/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/checkSignupStatus.ts new file mode 100644 index 0000000..e531830 --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/checkSignupStatus.ts @@ -0,0 +1,30 @@ +async function checkSignupStatus( + setIsSignedUp: React.Dispatch>, + setAssignednickname: React.Dispatch>, +) { + try { + const response = await fetch("/api/users/guestloginactivity", { + method: "GET", + cache: "no-cache", + }); + + if (!response.ok) { + throw new Error("회원가입 상태 확인에 실패했습니다."); + } + + const result = await response.json(); + const isSigned = result.data.loggedInBefore; + setIsSignedUp(isSigned); + if (isSigned) { + setAssignednickname(result.data.nickname.replace(/^익명의 /, "")); + } + } catch (error) { + if (error instanceof Error) { + console.error("오류 발생:", error.message); + } else { + console.error("알 수 없는 오류 발생:", error); + } + } +} + +export { checkSignupStatus }; diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/handleGuestSignin.ts b/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/handleGuestSignin.ts new file mode 100644 index 0000000..e687f34 --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/handleGuestSignin.ts @@ -0,0 +1,18 @@ +async function handleGuestSignin(nickname: string) { + try { + const response = await fetch("/api/users/guestsignin", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ nickname: `익명의 ${nickname}` }), + }); + if (!response.ok) throw new Error("게스트 로그인에 실패했습니다."); + const { data } = await response.json(); + return data; + } catch (error) { + console.error("게스트 로그인에 실패했습니다.", error); + } +} + +export { handleGuestSignin }; diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/index.ts b/frontend/src/pages/login-page/ui/components/GuestLoginForm/index.ts new file mode 100644 index 0000000..010e31c --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/index.ts @@ -0,0 +1 @@ +export { GuestLoginForm } from "./ui/GuestLoginForm"; diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/GuestLoginForm.tsx b/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/GuestLoginForm.tsx new file mode 100644 index 0000000..5f95f57 --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/GuestLoginForm.tsx @@ -0,0 +1,85 @@ +import { useEffect, useRef, useState } from "react"; +import { useAuthStore } from "../../../../model/store"; +import { Warning } from "../../Warning"; +import { useNavigate } from "@tanstack/react-router"; +import { useQueryClient } from "@tanstack/react-query"; +import { authQueries } from "@/shared/lib/auth/authQuery"; +import { useAuthState } from "@/shared/hooks/useAuthState"; +import { Input } from "./Input"; +import { checkSignupStatus } from "../api/checkSignupStatus"; +import { handleGuestSignin } from "../api/handleGuestSignin"; +import { updateQueryClient } from "@/shared/lib/updateQueryClient"; + +function GuestLoginForm({ to, roomId }: { to?: string; roomId?: string }) { + const [assignednickname, setAssignednickname] = useState(""); + const { error } = useAuthStore(); + const [issignedup, setIssignedup] = useState(false); + const queryClient = useQueryClient(); + const { setAuthState } = useAuthState(); + const inputRef = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + checkSignupStatus(setIssignedup, setAssignednickname); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + if (!inputRef.current) return; + e.preventDefault(); + const nickname = inputRef.current?.value; + const response = await handleGuestSignin(`익명의 ${nickname}`); + if (!response.ok) { + console.error("게스트 로그인에 실패했습니다."); + return; + } + updateQueryClient(queryClient, authQueries.queryKey, (prev) => ({ + ...prev, + userInfo: { + role: "guest", + nickname: nickname, + duck: 0, + message: "OK", + realDuck: prev.userInfo.realDuck, + }, + })); + + setAuthState((prev) => ({ + ...prev, + isAuthenticated: true, + nickname: nickname, + })); + + if (to && roomId) { + window.location.href = `/betting/${roomId}/waiting`; + } + + navigate({ + to: "/my-page", + }); + }; + return ( +
+
+
+ +
+
+ {issignedup && ( + + )} + {error && } + + + ); +} + +export { GuestLoginForm }; diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/Input.tsx b/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/Input.tsx new file mode 100644 index 0000000..bc8a211 --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/Input.tsx @@ -0,0 +1,39 @@ +// Input.tsx +import { forwardRef, useState } from "react"; +import { LoginIDIcon } from "@/shared/icons"; + +interface InputProps extends React.InputHTMLAttributes { + assignednickname: string; + issignedup: boolean; +} + +const Input = forwardRef((props, ref) => { + const { assignednickname, issignedup, ...rest } = props; + const [nickname, setNickname] = useState(assignednickname); + + return ( + + ); +}); + +export { Input }; diff --git a/frontend/src/features/login-page/ui/components/LoginForm.tsx b/frontend/src/pages/login-page/ui/components/LoginForm.tsx similarity index 93% rename from frontend/src/features/login-page/ui/components/LoginForm.tsx rename to frontend/src/pages/login-page/ui/components/LoginForm.tsx index b5e8abc..72da555 100644 --- a/frontend/src/features/login-page/ui/components/LoginForm.tsx +++ b/frontend/src/pages/login-page/ui/components/LoginForm.tsx @@ -9,6 +9,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { authQueries } from "@/shared/lib/auth/authQuery"; import { z } from "zod"; import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { useAuthState } from "@/shared/hooks/useAuthState"; function LoginForm() { const [email, setEmail] = useState(""); @@ -18,6 +19,7 @@ function LoginForm() { const { setUserInfo } = useUserContext(); const { error, handleLogin } = useAuthStore(); const queryClient = useQueryClient(); + const { setAuthState } = useAuthState(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -44,6 +46,12 @@ function LoginForm() { }), ); await queryClient.invalidateQueries({ queryKey: authQueries.queryKey }); + setAuthState((prev) => ({ + ...prev, + isAuthenticated: true, + nickname: data.nickname, + })); + navigate({ to: "/my-page", search: { diff --git a/frontend/src/pages/login-page/ui/components/Logout.tsx b/frontend/src/pages/login-page/ui/components/Logout.tsx new file mode 100644 index 0000000..9be9a1b --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/Logout.tsx @@ -0,0 +1,23 @@ +import { LogoutIcon } from "@/shared/icons/Logout"; +import { useLogout } from "@/shared/hooks/useLogout"; + +function LogoutButton() { + const logout = useLogout(); + + return ( + + ); +} + +export { LogoutButton }; diff --git a/frontend/src/features/login-page/ui/components/RegisterForm.tsx b/frontend/src/pages/login-page/ui/components/RegisterForm.tsx similarity index 97% rename from frontend/src/features/login-page/ui/components/RegisterForm.tsx rename to frontend/src/pages/login-page/ui/components/RegisterForm.tsx index 4f0a0bf..17fcfa8 100644 --- a/frontend/src/features/login-page/ui/components/RegisterForm.tsx +++ b/frontend/src/pages/login-page/ui/components/RegisterForm.tsx @@ -1,13 +1,13 @@ import { InputField } from "@/shared/components/input/InputField"; import { EmailIcon, LoginIDIcon, LoginPasswordIcon } from "@/shared/icons"; import { useEffect, useState } from "react"; -import { useAuthStore } from "@/features/login-page/model/store"; +import { useAuthStore } from "@/pages/login-page/model/store"; import { validateConfirmPassword, validateEmail, validateNickname, validatePassword, -} from "@/features/login-page/model/validation"; +} from "@/pages/login-page/model/validation"; import { Warning } from "./Warning"; interface RegisterFormProps { diff --git a/frontend/src/features/login-page/ui/components/TabButton.tsx b/frontend/src/pages/login-page/ui/components/TabButton.tsx similarity index 100% rename from frontend/src/features/login-page/ui/components/TabButton.tsx rename to frontend/src/pages/login-page/ui/components/TabButton.tsx diff --git a/frontend/src/features/login-page/ui/components/Warning.tsx b/frontend/src/pages/login-page/ui/components/Warning.tsx similarity index 100% rename from frontend/src/features/login-page/ui/components/Warning.tsx rename to frontend/src/pages/login-page/ui/components/Warning.tsx diff --git a/frontend/src/features/login-page/ui/components/index.ts b/frontend/src/pages/login-page/ui/components/index.ts similarity index 100% rename from frontend/src/features/login-page/ui/components/index.ts rename to frontend/src/pages/login-page/ui/components/index.ts diff --git a/frontend/src/pages/my-page/api/purchaseDuck.ts b/frontend/src/pages/my-page/api/purchaseDuck.ts new file mode 100644 index 0000000..bf55800 --- /dev/null +++ b/frontend/src/pages/my-page/api/purchaseDuck.ts @@ -0,0 +1,24 @@ +import { QueryClient } from "@tanstack/react-query"; +import { userInfoQueries } from "@/shared/lib/auth/authQuery"; +import { updateQueryClient } from "@/shared/lib/updateQueryClient"; + +async function purchaseDuck(currentDuck: number, queryClient: QueryClient) { + if (currentDuck < 30) { + return; + } + + const response = await fetch("/api/users/purchaseduck", { + cache: "no-cache", + }); + if (!response.ok) { + console.error("Failed to purchase duck"); + return; + } + updateQueryClient(queryClient, userInfoQueries.queryKey, (prev) => ({ + ...prev, + duck: prev.userInfo.duck - 30, + realDuck: prev.userInfo.realDuck + 1, + })); +} + +export { purchaseDuck }; diff --git a/frontend/src/pages/my-page/api/updateDuckCountInCache.ts b/frontend/src/pages/my-page/api/updateDuckCountInCache.ts new file mode 100644 index 0000000..e4f8df2 --- /dev/null +++ b/frontend/src/pages/my-page/api/updateDuckCountInCache.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { authQueries } from "@/shared/lib/auth/authQuery"; +import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { useQueryClient } from "@tanstack/react-query"; + +async function updateDuckCountInCache( + queryClient: ReturnType, + { + currentDuck, + numberOfDucks, + }: { currentDuck: number; numberOfDucks: number }, +) { + await queryClient.setQueryData( + authQueries.queryKey, + (old: z.infer) => ({ + ...old, + userInfo: { + ...old.userInfo, + duck: currentDuck - 30, + realDuck: numberOfDucks + 1, + }, + }), + ); +} + +export { updateDuckCountInCache }; diff --git a/frontend/src/features/my-page/error/index.tsx b/frontend/src/pages/my-page/error/index.tsx similarity index 84% rename from frontend/src/features/my-page/error/index.tsx rename to frontend/src/pages/my-page/error/index.tsx index 8e87253..a771375 100644 --- a/frontend/src/features/my-page/error/index.tsx +++ b/frontend/src/pages/my-page/error/index.tsx @@ -1,5 +1,3 @@ -import { Image } from "@/shared/components/Image"; -import pondImage from "@/assets/images/pond.png"; import { DuckCoinIcon } from "@/shared/icons"; function ErrorMyPage() { @@ -13,9 +11,7 @@ function ErrorMyPage() { {0} -
- Pond -
+
); -}; +}); export { AnimatedDuckCount }; diff --git a/frontend/src/pages/my-page/ui/FallingDuck.tsx b/frontend/src/pages/my-page/ui/FallingDuck.tsx new file mode 100644 index 0000000..bd3de41 --- /dev/null +++ b/frontend/src/pages/my-page/ui/FallingDuck.tsx @@ -0,0 +1,33 @@ +import { Group } from "three"; +import { useBox } from "@react-three/cannon"; +import { Gltf } from "@react-three/drei"; +import duckModel from "@/assets/models/betting-duck.glb"; +import { memo } from "react"; + +const FallingDuck = memo(() => { + const randomX = (Math.random() - 0.5) * 2; + const randomZ = (Math.random() - 0.5) * 2; + + const [ref] = useBox(() => ({ + mass: 1, + position: [randomX, 5, randomZ], + rotation: [Math.random() * 0.2, Math.random() * 0.2, Math.random() * 0.2], + linearDamping: 0.4, + angularDamping: 0.4, + material: { + friction: 0.3, + restitution: 0.3, + }, + })); + + return ( + } + castShadow + receiveShadow + src={duckModel} + /> + ); +}); + +export default FallingDuck; diff --git a/frontend/src/pages/my-page/ui/MyPage.tsx b/frontend/src/pages/my-page/ui/MyPage.tsx new file mode 100644 index 0000000..7744e58 --- /dev/null +++ b/frontend/src/pages/my-page/ui/MyPage.tsx @@ -0,0 +1,44 @@ +import { Suspense, lazy } from "react"; +import { useNavigate } from "@tanstack/react-router"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { userInfoQueries } from "@/shared/lib/auth/authQuery"; +import { AnimatedDuckCount } from "./AnimatedDuckCount"; +import { PurchaseButton } from "./PurchaseButton"; + +const Pond = lazy(() => import("./Pond")); + +function MyPage() { + const { data: authData } = useSuspenseQuery({ + queryKey: userInfoQueries.queryKey, + queryFn: userInfoQueries.queryFn, + }); + const navigate = useNavigate(); + + return ( +
+
+
+

마이 페이지

+

오리를 구매해서 페이지를 꾸며보세요

+
+ + +
+ +
+
+
+ + +
+
+
+ ); +} + +export { MyPage }; diff --git a/frontend/src/features/my-page/ui/Pond.tsx b/frontend/src/pages/my-page/ui/Pond.tsx similarity index 53% rename from frontend/src/features/my-page/ui/Pond.tsx rename to frontend/src/pages/my-page/ui/Pond.tsx index bd57380..e854f1d 100644 --- a/frontend/src/features/my-page/ui/Pond.tsx +++ b/frontend/src/pages/my-page/ui/Pond.tsx @@ -1,4 +1,4 @@ -import * as THREE from "three"; +import { Mesh } from "three"; import { Canvas } from "@react-three/fiber"; import { OrbitControls, @@ -6,11 +6,12 @@ import { OrthographicCamera, } from "@react-three/drei"; import { Physics, usePlane } from "@react-three/cannon"; -import { Suspense } from "react"; -import envMap from "@assets/models/industrial_sunset_puresky_4k.hdr"; +import { lazy, memo, Suspense, useCallback, useEffect, useState } from "react"; -function Ground({ color = "#f0f4fa" }: { color?: string }) { - const [ref] = usePlane(() => ({ +const FallingDuck = lazy(() => import("./FallingDuck")); + +const Ground = memo(() => { + const [ref] = usePlane(() => ({ rotation: [-Math.PI / 2, 0, 0], position: [0, -6.5, 0], type: "Static", @@ -23,12 +24,48 @@ function Ground({ color = "#f0f4fa" }: { color?: string }) { return ( - + ); -} +}); + +function Pond({ realDuck }: { realDuck: number }) { + const [envMap, setEnvMap] = useState(null); + const [duckModels, setDuckModels] = useState([FallingDuck]); + + const addDuck = useCallback((count: number, remainDucks: number) => { + if (count >= remainDucks) return; + const timer = setTimeout(() => { + setDuckModels((prevDucks) => [...prevDucks, FallingDuck]); + addDuck(count + 1, remainDucks); + }, 200); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + const remainDucks = realDuck - duckModels.length; + if (remainDucks <= 0) return; + + const initialTimer = setTimeout(() => { + addDuck(0, remainDucks); + }, 1000); + + return () => clearTimeout(initialTimer); + }, [realDuck, duckModels.length, addDuck]); + + useEffect(() => { + (async () => { + const env = await import( + "@assets/models/industrial_sunset_puresky_4k.hdr" + ); + setEnvMap(env.default); + })(); + }, []); -function Pond({ ducks }: { ducks: React.ElementType[] }) { return ( - {ducks.map((DuckComponent, index) => ( + {duckModels.map((DuckComponent, index) => ( ))} - + - + {envMap && } ; +} + +const PurchaseButton = memo(({ authData }: PurchaseButtonProps) => { + const queryClient = useQueryClient(); + + return ( + + ); +}); + +export { PurchaseButton }; diff --git a/frontend/src/features/predict-detail/index.tsx b/frontend/src/pages/predict-detail/index.tsx similarity index 100% rename from frontend/src/features/predict-detail/index.tsx rename to frontend/src/pages/predict-detail/index.tsx diff --git a/frontend/src/features/predict-detail/model/api.ts b/frontend/src/pages/predict-detail/model/api.ts similarity index 100% rename from frontend/src/features/predict-detail/model/api.ts rename to frontend/src/pages/predict-detail/model/api.ts diff --git a/frontend/src/features/predict-detail/model/schema.ts b/frontend/src/pages/predict-detail/model/schema.ts similarity index 100% rename from frontend/src/features/predict-detail/model/schema.ts rename to frontend/src/pages/predict-detail/model/schema.ts diff --git a/frontend/src/features/predict-detail/ui/AdminBettingResult.tsx b/frontend/src/pages/predict-detail/ui/AdminBettingResult.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/AdminBettingResult.tsx rename to frontend/src/pages/predict-detail/ui/AdminBettingResult.tsx diff --git a/frontend/src/features/predict-detail/ui/Bettingstatistics.tsx b/frontend/src/pages/predict-detail/ui/Bettingstatistics.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/Bettingstatistics.tsx rename to frontend/src/pages/predict-detail/ui/Bettingstatistics.tsx diff --git a/frontend/src/features/predict-detail/ui/GuestFooter.tsx b/frontend/src/pages/predict-detail/ui/GuestFooter.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/GuestFooter.tsx rename to frontend/src/pages/predict-detail/ui/GuestFooter.tsx diff --git a/frontend/src/features/predict-detail/ui/UserBettingResult.tsx b/frontend/src/pages/predict-detail/ui/UserBettingResult.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/UserBettingResult.tsx rename to frontend/src/pages/predict-detail/ui/UserBettingResult.tsx diff --git a/frontend/src/features/predict-detail/ui/UserFooter.tsx b/frontend/src/pages/predict-detail/ui/UserFooter.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/UserFooter.tsx rename to frontend/src/pages/predict-detail/ui/UserFooter.tsx diff --git a/frontend/src/features/waiting-room/error/AccessError.ts b/frontend/src/pages/waiting-room/error/AccessError.ts similarity index 100% rename from frontend/src/features/waiting-room/error/AccessError.ts rename to frontend/src/pages/waiting-room/error/AccessError.ts diff --git a/frontend/src/features/waiting-room/error/Forbidden.tsx b/frontend/src/pages/waiting-room/error/Forbidden.tsx similarity index 100% rename from frontend/src/features/waiting-room/error/Forbidden.tsx rename to frontend/src/pages/waiting-room/error/Forbidden.tsx diff --git a/frontend/src/features/waiting-room/error/Unauthorized.tsx b/frontend/src/pages/waiting-room/error/Unauthorized.tsx similarity index 100% rename from frontend/src/features/waiting-room/error/Unauthorized.tsx rename to frontend/src/pages/waiting-room/error/Unauthorized.tsx diff --git a/frontend/src/features/waiting-room/hooks/use-waiting-context.tsx b/frontend/src/pages/waiting-room/hooks/use-waiting-context.tsx similarity index 100% rename from frontend/src/features/waiting-room/hooks/use-waiting-context.tsx rename to frontend/src/pages/waiting-room/hooks/use-waiting-context.tsx diff --git a/frontend/src/features/waiting-room/index.tsx b/frontend/src/pages/waiting-room/index.tsx similarity index 97% rename from frontend/src/features/waiting-room/index.tsx rename to frontend/src/pages/waiting-room/index.tsx index 49a2412..33a1612 100644 --- a/frontend/src/features/waiting-room/index.tsx +++ b/frontend/src/pages/waiting-room/index.tsx @@ -1,6 +1,6 @@ import { WaitingRoomHeader } from "./ui/WaitingRoomHeader"; import { ParticipantsList } from "./ui/ParticipantsList"; -import { ShareLinkCard } from "@/features/waiting-room/ui/SharedLinkCard"; +import { ShareLinkCard } from "@/pages/waiting-room/ui/SharedLinkCard"; import { AdminFooter } from "./ui/AdminFooter"; import { MemberFooter } from "./ui/MemberFooter"; import { WaitingRoomProvider } from "./provider/WaitingRoomProvider"; diff --git a/frontend/src/features/waiting-room/provider/WaitingRoomProvider.tsx b/frontend/src/pages/waiting-room/provider/WaitingRoomProvider.tsx similarity index 100% rename from frontend/src/features/waiting-room/provider/WaitingRoomProvider.tsx rename to frontend/src/pages/waiting-room/provider/WaitingRoomProvider.tsx diff --git a/frontend/src/features/waiting-room/style.module.css b/frontend/src/pages/waiting-room/style.module.css similarity index 100% rename from frontend/src/features/waiting-room/style.module.css rename to frontend/src/pages/waiting-room/style.module.css diff --git a/frontend/src/features/waiting-room/ui/AdminFooter/CancleButton.tsx b/frontend/src/pages/waiting-room/ui/AdminFooter/CancleButton.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/AdminFooter/CancleButton.tsx rename to frontend/src/pages/waiting-room/ui/AdminFooter/CancleButton.tsx diff --git a/frontend/src/features/waiting-room/ui/AdminFooter/ParticipateButton.tsx b/frontend/src/pages/waiting-room/ui/AdminFooter/ParticipateButton.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/AdminFooter/ParticipateButton.tsx rename to frontend/src/pages/waiting-room/ui/AdminFooter/ParticipateButton.tsx diff --git a/frontend/src/features/waiting-room/ui/AdminFooter/StartVotingButton.tsx b/frontend/src/pages/waiting-room/ui/AdminFooter/StartVotingButton.tsx similarity index 94% rename from frontend/src/features/waiting-room/ui/AdminFooter/StartVotingButton.tsx rename to frontend/src/pages/waiting-room/ui/AdminFooter/StartVotingButton.tsx index 16c0f16..8c4cc9e 100644 --- a/frontend/src/features/waiting-room/ui/AdminFooter/StartVotingButton.tsx +++ b/frontend/src/pages/waiting-room/ui/AdminFooter/StartVotingButton.tsx @@ -1,7 +1,7 @@ import { useNavigate, useRouter } from "@tanstack/react-router"; import { responseBetRoomInfo } from "@betting-duck/shared"; import { z } from "zod"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; import React from "react"; function StartVotingButton({ diff --git a/frontend/src/features/waiting-room/ui/AdminFooter/index.tsx b/frontend/src/pages/waiting-room/ui/AdminFooter/index.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/AdminFooter/index.tsx rename to frontend/src/pages/waiting-room/ui/AdminFooter/index.tsx diff --git a/frontend/src/features/waiting-room/ui/MemberFooter/index.tsx b/frontend/src/pages/waiting-room/ui/MemberFooter/index.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/MemberFooter/index.tsx rename to frontend/src/pages/waiting-room/ui/MemberFooter/index.tsx diff --git a/frontend/src/features/waiting-room/ui/ParticipantsList.tsx b/frontend/src/pages/waiting-room/ui/ParticipantsList.tsx similarity index 97% rename from frontend/src/features/waiting-room/ui/ParticipantsList.tsx rename to frontend/src/pages/waiting-room/ui/ParticipantsList.tsx index 4af9640..d060b56 100644 --- a/frontend/src/features/waiting-room/ui/ParticipantsList.tsx +++ b/frontend/src/pages/waiting-room/ui/ParticipantsList.tsx @@ -1,4 +1,4 @@ -import waitingUserImage from "@assets/images/waiting-user.png"; +import waitingUserImage from "@assets/images/waiting-user.avif"; import React from "react"; import { useWaitingContext } from "../hooks/use-waiting-context"; import { z } from "zod"; diff --git a/frontend/src/features/waiting-room/ui/SharedLinkCard.tsx b/frontend/src/pages/waiting-room/ui/SharedLinkCard.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/SharedLinkCard.tsx rename to frontend/src/pages/waiting-room/ui/SharedLinkCard.tsx diff --git a/frontend/src/features/waiting-room/ui/WaitingError/index.tsx b/frontend/src/pages/waiting-room/ui/WaitingError/index.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/WaitingError/index.tsx rename to frontend/src/pages/waiting-room/ui/WaitingError/index.tsx diff --git a/frontend/src/features/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx b/frontend/src/pages/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx similarity index 98% rename from frontend/src/features/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx rename to frontend/src/pages/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx index b64da35..0fb6a88 100644 --- a/frontend/src/features/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx +++ b/frontend/src/pages/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx @@ -6,7 +6,7 @@ import { CoinInput, Timer, TitleInput, -} from "@/features/create-vote/ui/components"; +} from "@/pages/create-vote/ui/components"; import { DialogContext } from "@/shared/components/Dialog"; import { useQueryClient } from "@tanstack/react-query"; import { bettingRoomQueryKey } from "@/shared/lib/bettingRoomInfo"; diff --git a/frontend/src/features/waiting-room/ui/WaitingRoomHeader/VotingStatusCard.tsx b/frontend/src/pages/waiting-room/ui/WaitingRoomHeader/VotingStatusCard.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/WaitingRoomHeader/VotingStatusCard.tsx rename to frontend/src/pages/waiting-room/ui/WaitingRoomHeader/VotingStatusCard.tsx diff --git a/frontend/src/features/waiting-room/ui/WaitingRoomHeader/index.tsx b/frontend/src/pages/waiting-room/ui/WaitingRoomHeader/index.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/WaitingRoomHeader/index.tsx rename to frontend/src/pages/waiting-room/ui/WaitingRoomHeader/index.tsx diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 2e16d67..08809f4 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,54 +1,23 @@ +import { Suspense } from "react"; import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; -import { RootHeader } from "@/shared/components/RootHeader"; -import { RootSideBar } from "@/shared/components/RootSideBar"; import { UserProvider } from "@/app/provider/UserProvider"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; -import { cn } from "@/shared/misc"; -import { useLayout } from "@/shared/hooks/useLayout"; import { LayoutProvider } from "@/app/provider/LayoutProvider"; -import { RouterContext } from "@/main"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import React from "react"; +import { RouterContext } from "@/app/provider/RouterProvider"; import { LoadingAnimation } from "@/shared/components/Loading"; +import { Layout } from "@/app/layout"; export const Route = createRootRouteWithContext()({ component: () => ( - - }> - - - - - + + }> + + + ), - loader: (opts) => opts.context.queryClient.ensureQueryData(authQueries), errorComponent: ({ error }) => , }); - -const layoutStyles = { - default: "max-w-[520px]", - wide: "max-w-[1200px]", -} as const; - -function RootLayout({ children }: { children: React.ReactNode }) { - const { layoutType } = useLayout(); - - return ( -
- {children} -
- ); -} - -export { RootLayout }; diff --git a/frontend/src/routes/betting.index.tsx b/frontend/src/routes/betting.index.tsx index a416763..d6fc1b7 100644 --- a/frontend/src/routes/betting.index.tsx +++ b/frontend/src/routes/betting.index.tsx @@ -1,19 +1,12 @@ import React from "react"; -import { WaitingError } from "@/features/waiting-room/ui/WaitingError"; +import { WaitingError } from "@/pages/waiting-room/ui/WaitingError"; import { useUserContext } from "@/shared/hooks/useUserContext"; -import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { responseBetRoomInfo } from "@betting-duck/shared"; +import { ProtectedRoute } from "@/shared/components/ProtectedRoute"; export const Route = createFileRoute("/betting/")({ component: RouteComponent, - beforeLoad: async () => { - const response = await fetch("/api/users/token"); - if (!response.ok) { - throw redirect({ - to: "/require-bettingRoomId", - }); - } - }, errorComponent: ({ error }) => ( @@ -49,7 +42,6 @@ function RouteComponent() { const { channel } = result.data; - // 상태에 따른 리다이렉트 처리 const redirectMap = { finished: "/require-bettingRoomId", waiting: `/betting/${roomId}/waiting`, @@ -79,7 +71,11 @@ function RouteComponent() { }, [roomId, navigate]); // 로딩 중 표시 - return
; + return ( + +
+ + ); } function ErrorComponent({ diff --git a/frontend/src/routes/betting_.$roomId.vote.admin.tsx b/frontend/src/routes/betting_.$roomId.vote.admin.tsx index 6c1f82a..2cecaad 100644 --- a/frontend/src/routes/betting_.$roomId.vote.admin.tsx +++ b/frontend/src/routes/betting_.$roomId.vote.admin.tsx @@ -1,4 +1,4 @@ -import { BettingPageAdmin } from "@/features/betting-page-admin"; +import { BettingPageAdmin } from "@/pages/betting-page-admin"; import { createFileRoute, redirect } from "@tanstack/react-router"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; import { validateRoomAccess } from "@/shared/utils/roomValidation"; diff --git a/frontend/src/routes/betting_.$roomId.vote.resultDetail.tsx b/frontend/src/routes/betting_.$roomId.vote.resultDetail.tsx index 4737bff..f469eef 100644 --- a/frontend/src/routes/betting_.$roomId.vote.resultDetail.tsx +++ b/frontend/src/routes/betting_.$roomId.vote.resultDetail.tsx @@ -1,5 +1,5 @@ -import { STORAGE_KEY } from "@/features/betting-page/model/var"; -import { PredictDetail } from "@/features/predict-detail"; +import { STORAGE_KEY } from "@/pages/betting-page/model/var"; +import { PredictDetail } from "@/pages/predict-detail"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; import { getSessionItem } from "@/shared/hooks/useSessionStorage"; import { BettingRoomInfoSchema } from "@/shared/types"; diff --git a/frontend/src/routes/betting_.$roomId.vote.tsx b/frontend/src/routes/betting_.$roomId.vote.tsx index f65df82..17d567e 100644 --- a/frontend/src/routes/betting_.$roomId.vote.tsx +++ b/frontend/src/routes/betting_.$roomId.vote.tsx @@ -3,17 +3,17 @@ import { Outlet, ErrorComponent, } from "@tanstack/react-router"; -import { Chat } from "@/features/chat"; +import { Chat } from "@/pages/chat"; import { cn } from "@/shared/misc"; -import { AccessError } from "@/features/waiting-room/error/AccessError"; -import { Unauthorized } from "@/features/waiting-room/error/Unauthorized"; -import { Forbidden } from "@/features/waiting-room/error/Forbidden"; +import { AccessError } from "@/pages/waiting-room/error/AccessError"; +import { Unauthorized } from "@/pages/waiting-room/error/Unauthorized"; +import { Forbidden } from "@/pages/waiting-room/error/Forbidden"; import { Suspense } from "react"; import { LoadingAnimation } from "@/shared/components/Loading"; import { bettingRoomQueryKey } from "@/shared/lib/bettingRoomInfo"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { BettingProvider } from "@/features/betting-page/provider/BettingProvider"; +import { BettingProvider } from "@/pages/betting-page/provider/BettingProvider"; interface RouteLoaderData { roomId: string; diff --git a/frontend/src/routes/betting_.$roomId.vote.voting.tsx b/frontend/src/routes/betting_.$roomId.vote.voting.tsx index 22010ac..3e2f118 100644 --- a/frontend/src/routes/betting_.$roomId.vote.voting.tsx +++ b/frontend/src/routes/betting_.$roomId.vote.voting.tsx @@ -1,6 +1,6 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; -import { BettingPage } from "@/features/betting-page"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { BettingPage } from "@/pages/betting-page"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; export const Route = createFileRoute("/betting_/$roomId/vote/voting")({ component: BettingPage, diff --git a/frontend/src/routes/betting_.$roomId.waiting.tsx b/frontend/src/routes/betting_.$roomId.waiting.tsx index 069e963..7472a5e 100644 --- a/frontend/src/routes/betting_.$roomId.waiting.tsx +++ b/frontend/src/routes/betting_.$roomId.waiting.tsx @@ -1,11 +1,11 @@ import { createFileRoute } from "@tanstack/react-router"; -import { WaitingRoom } from "@/features/waiting-room"; -import { WaitingError } from "@/features/waiting-room/ui/WaitingError"; -import { AccessError } from "@/features/waiting-room/error/AccessError"; -import { Forbidden } from "@/features/waiting-room/error/Forbidden"; +import { WaitingRoom } from "@/pages/waiting-room"; +import { WaitingError } from "@/pages/waiting-room/ui/WaitingError"; +import { AccessError } from "@/pages/waiting-room/error/AccessError"; +import { Forbidden } from "@/pages/waiting-room/error/Forbidden"; import { ErrorComponent } from "@/shared/components/Error"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; -import { GuestLoginForm } from "@/features/login-page/ui/components"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; +import { GuestLoginForm } from "@/pages/login-page/ui/components"; import { bettingRoomQueryKey } from "@/shared/lib/bettingRoomInfo"; import { getSessionItem, diff --git a/frontend/src/routes/create-vote.tsx b/frontend/src/routes/create-vote.tsx index edfd7f3..0c92578 100644 --- a/frontend/src/routes/create-vote.tsx +++ b/frontend/src/routes/create-vote.tsx @@ -1,16 +1,56 @@ -import { CreateVotePage } from "@/features/create-vote"; +import { CreateVotePage } from "@/pages/create-vote"; import { createFileRoute, redirect } from "@tanstack/react-router"; import { ErrorComponent } from "@/shared/components/Error"; -import { CreateVoteError } from "@/features/create-vote/ui/error/CreateVoteError"; +import { CreateVoteError } from "@/pages/create-vote/ui/error/CreateVoteError"; import { getSessionItem } from "@/shared/hooks/useSessionStorage"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; import { ROUTES } from "@/shared/config/route"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { ProtectedRoute } from "@/shared/components/ProtectedRoute"; + +type RoomInfo = Awaited>; + +function redirectWaitingRoom(roomInfo: RoomInfo, roomId: string) { + if (roomInfo?.channel.status === "waiting") { + return redirect({ + to: "/betting/$roomId/waiting", + params: { roomId }, + }); + } +} + +function redirectVoting(roomInfo: RoomInfo, roomId: string) { + if (roomInfo?.channel.status === "active") { + return redirect({ + to: "/betting/$roomId/vote/voting", + params: { roomId }, + }); + } +} + +function redirectGuest(role: string) { + if (role === "guest") { + return redirect({ + to: "/require-login", + search: { from: encodeURIComponent(ROUTES.GUEST_CREATE_VOTE) }, + }); + } +} + +function redirectFinished(roomInfo: RoomInfo, roomId: string) { + if ( + roomInfo?.channel.status === "finished" || + roomInfo?.channel.status === "timeover" + ) { + return redirect({ + to: "/betting/$roomId/vote/resultDetail", + params: { roomId }, + }); + } +} export const Route = createFileRoute("/create-vote")({ component: RouteComponent, - beforeLoad: async ({ context: { queryClient } }) => { + beforeLoad: async () => { let roomId; try { const sessionUserInfo = await getSessionItem("userInfo"); @@ -18,60 +58,17 @@ export const Route = createFileRoute("/create-vote")({ roomId = parsedInfo?.roomId; const roomInfo = await getBettingRoomInfo(roomId); if (!roomId) { - return; // roomId가 없으면 조기 반환 + return; } if (!roomInfo) { - return; // roomInfo가 없으면 조기 반환 + return; } - // 상태에 따른 리다이렉트 처리 - if (roomInfo.channel.status === "waiting") { - return redirect({ - to: "/betting/$roomId/waiting", - params: { roomId }, - }); - } - - if (roomInfo.channel.status === "active") { - return redirect({ - to: "/betting/$roomId/vote/voting", - params: { roomId }, - }); - } - - if (parsedInfo.role === "guest") { - return redirect({ - to: "/require-login", - search: { from: encodeURIComponent(ROUTES.GUEST_CREATE_VOTE) }, - }); - } - - if ( - roomInfo.channel.status === "finished" || - roomInfo.channel.status === "timeover" - ) { - return redirect({ - to: "/betting/$roomId/vote/resultDetail", - params: { roomId }, - }); - } - - await queryClient.ensureQueryData(authQueries); - const queryClientData = await queryClient.getQueryData( - authQueries.queryKey, - ); - const parsedQueryClientData = - AuthStatusTypeSchema.safeParse(queryClientData); - if ( - parsedQueryClientData.success && - parsedQueryClientData.data.userInfo.role === "guest" - ) { - return redirect({ - to: "/require-login", - search: { from: encodeURIComponent(ROUTES.GUEST_CREATE_VOTE) }, - }); - } + redirectWaitingRoom(roomInfo, roomId); + redirectVoting(roomInfo, roomId); + redirectGuest(parsedInfo.role); + redirectFinished(roomInfo, roomId); } catch (error) { if (error instanceof Error && error.name === "RedirectError") { throw error; @@ -87,5 +84,9 @@ export const Route = createFileRoute("/create-vote")({ }); function RouteComponent() { - return ; + return ( + + ; + + ); } diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 0d77680..f1b0271 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -1,40 +1,18 @@ -import { - Dialog, - DialogTrigger, - DialogContent, -} from "@/shared/components/Dialog"; +import { authQueries } from "@/shared/lib/auth/authQuery"; import { createFileRoute, redirect } from "@tanstack/react-router"; -const user = undefined; - export const Route = createFileRoute("/")({ - loader: () => { - if (!user) { + beforeLoad: ({ context }) => { + const { queryClient } = context; + const auth = queryClient.getQueryData(authQueries.queryKey); + if (!auth) { throw redirect({ to: "/login", }); } + + throw redirect({ + to: "/my-page", + }); }, - component: () => ( - - - - - -
-

기본 Dialog

-
-
-

Dialog 내용이 여기에 들어갑니다.

-
-
- -
-
-
- ), }); diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index 23a1475..164ab43 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -1,24 +1,13 @@ -import { LoginPage } from "@/features/login-page"; +import { LoginPage } from "@/pages/login-page"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; -import { createFileRoute, redirect } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; +import { ProtectedRoute } from "@/shared/components/ProtectedRoute"; export const Route = createFileRoute("/login")({ - component: Component, - loader: async ({ context: { queryClient } }) => { - const queryData = await queryClient.ensureQueryData(authQueries); - const parsedData = AuthStatusTypeSchema.safeParse(queryData); - - if (parsedData.success && parsedData.data.isAuthenticated) { - return redirect({ - to: "/my-page", - }); - } - }, + component: () => ( + + + + ), errorComponent: ({ error }) => , }); - -function Component() { - return ; -} diff --git a/frontend/src/routes/my-page.tsx b/frontend/src/routes/my-page.tsx index 18c14de..b8e37bc 100644 --- a/frontend/src/routes/my-page.tsx +++ b/frontend/src/routes/my-page.tsx @@ -1,26 +1,17 @@ -import { createFileRoute, redirect } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; import { ErrorComponent } from "@/shared/components/Error"; -import { MyPage } from "@/features/my-page"; -import { ErrorMyPage } from "@/features/my-page/error"; -import { ROUTES } from "@/shared/config/route"; +import { MyPage } from "@/pages/my-page"; +import { ErrorMyPage } from "@/pages/my-page/error"; +import { ProtectedRoute } from "@/shared/components/ProtectedRoute"; export const Route = createFileRoute("/my-page")({ - beforeLoad: async () => { - const tokenResponse = await fetch("/api/users/token", { - headers: { - "Cache-Control": "stale-while-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - }); - if (!tokenResponse.ok) { - throw redirect({ - to: "/require-login", - search: { from: encodeURIComponent(ROUTES.MYPAGE) }, - }); - } + component: () => { + return ( + + + + ); }, - component: MyPage, shouldReload: () => true, errorComponent: ({ error }) => { return ( diff --git a/frontend/src/routes/require-login.tsx b/frontend/src/routes/require-login.tsx index 942151a..d2273d6 100644 --- a/frontend/src/routes/require-login.tsx +++ b/frontend/src/routes/require-login.tsx @@ -1,10 +1,9 @@ import { createFileRoute } from "@tanstack/react-router"; import { ErrorComponent } from "@/shared/components/Error"; -import { CreateVoteError } from "@/features/create-vote/ui/error/CreateVoteError"; +import { CreateVoteError } from "@/pages/create-vote/ui/error/CreateVoteError"; import { z } from "zod"; -import { ErrorMyPage } from "@/features/my-page/error"; import { ROUTE_PATH_ENUM, ROUTES } from "@/shared/config/route"; -import { WaitingError } from "@/features/waiting-room/ui/WaitingError"; +import { WaitingError } from "@/pages/waiting-room/ui/WaitingError"; import { GuestErrorComponent } from "@/shared/components/Error/GuestError"; const searchSchema = z.object({ @@ -45,7 +44,7 @@ function RouteComponent() { if (from === ROUTES.GUEST_LOGIN) { return ( - + <> ); } @@ -53,7 +52,7 @@ function RouteComponent() { if (from === ROUTES.MYPAGE) { return ( - + <> ); } diff --git a/frontend/src/shared/components/Error/index.tsx b/frontend/src/shared/components/Error/index.tsx index 2fc2c23..ab7019d 100644 --- a/frontend/src/shared/components/Error/index.tsx +++ b/frontend/src/shared/components/Error/index.tsx @@ -1,3 +1,5 @@ +import { useNavigate } from "@tanstack/react-router"; + function ErrorComponent({ error, to, @@ -9,6 +11,8 @@ function ErrorComponent({ children: React.ReactNode; feature?: string; }) { + const navigate = useNavigate(); + return (
diff --git a/frontend/src/shared/components/ProtectedRoute/index.ts b/frontend/src/shared/components/ProtectedRoute/index.ts new file mode 100644 index 0000000..1a219f5 --- /dev/null +++ b/frontend/src/shared/components/ProtectedRoute/index.ts @@ -0,0 +1 @@ +export { ProtectedRoute } from "./ui/ProtectedRoute"; diff --git a/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx b/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx new file mode 100644 index 0000000..f12bc10 --- /dev/null +++ b/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx @@ -0,0 +1,46 @@ +import { useRecoilValue } from "recoil"; +import { Auth } from "@/app/provider/RouterProvider/lib/auth"; +import { useNavigate, useLocation } from "@tanstack/react-router"; +import { useEffect } from "react"; +import { ROUTES } from "@/shared/config/route"; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export function ProtectedRoute({ children }: ProtectedRouteProps) { + const authState = useRecoilValue(Auth); + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + if (authState.isLoading) return; + + if (authState.isAuthenticated && location.pathname == ROUTES.LOGIN) { + navigate({ + to: "/my-page", + }); + } + + if (!authState.isAuthenticated && location.pathname == ROUTES.MYPAGE) { + navigate({ + to: "/require-login", + search: { from: encodeURIComponent(ROUTES.MYPAGE) }, + }); + } + + if (!authState.isAuthenticated && location.pathname == ROUTES.CREATE_VOTE) { + navigate({ + to: "/require-login", + search: { from: encodeURIComponent(ROUTES.CREATE_VOTE) }, + }); + } + }, [ + authState.isAuthenticated, + authState.isLoading, + navigate, + location.pathname, + ]); + + return <>{children}; +} diff --git a/frontend/src/shared/components/RootSideBar/index.tsx b/frontend/src/shared/components/RootSideBar/index.tsx index ff21c35..39bf156 100644 --- a/frontend/src/shared/components/RootSideBar/index.tsx +++ b/frontend/src/shared/components/RootSideBar/index.tsx @@ -8,7 +8,7 @@ import { import { NavItem } from "./item"; import styles from "./style.module.css"; import { useLocation } from "@tanstack/react-router"; -import { LogoutButton } from "@/features/login-page/ui/components/Logout"; +import { LogoutButton } from "@/pages/login-page/ui/components/Logout"; import { useQuery } from "@tanstack/react-query"; import { authQueries } from "@/shared/lib/auth/authQuery"; diff --git a/frontend/src/shared/hooks/useAuthState.ts b/frontend/src/shared/hooks/useAuthState.ts new file mode 100644 index 0000000..7557281 --- /dev/null +++ b/frontend/src/shared/hooks/useAuthState.ts @@ -0,0 +1,12 @@ +import { useRecoilState } from "recoil"; +import { Auth } from "@/app/provider/RouterProvider/lib/auth"; + +function useAuthState() { + const [authState, setAuthState] = useRecoilState(Auth); + if (!authState) { + throw new Error("AuthState가 존재하지 않습니다."); + } + return { authState, setAuthState }; +} + +export { useAuthState }; diff --git a/frontend/src/shared/hooks/useLogout.ts b/frontend/src/shared/hooks/useLogout.ts new file mode 100644 index 0000000..bbfb911 --- /dev/null +++ b/frontend/src/shared/hooks/useLogout.ts @@ -0,0 +1,71 @@ +import { z } from "zod"; +import { useCallback } from "react"; +import { useUserContext } from "@/shared/hooks/useUserContext"; +import { useSetRecoilState } from "recoil"; +import { useQueryClient } from "@tanstack/react-query"; +import { Auth } from "@/app/provider/RouterProvider/lib/auth"; +import { authQueries } from "@/shared/lib/auth/authQuery"; +import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { useNavigate } from "@tanstack/react-router"; + +export const useLogout = () => { + const navigate = useNavigate(); + const { setUserInfo } = useUserContext(); + const setAuthState = useSetRecoilState(Auth); + const queryClient = useQueryClient(); + + const logout = useCallback(async () => { + try { + const response = await fetch("/api/users/signout", { + credentials: "include", + cache: "no-cache", + }); + + if (!response.ok) { + throw new Error("로그아웃에 실패했습니다."); + } + + setAuthState((prev) => ({ + ...prev, + isAuthenticated: false, + isLoading: true, + nickname: "", + })); + + setUserInfo({ isAuthenticated: false, nickname: "", role: "guest" }); + + queryClient.setQueryData( + authQueries.queryKey, + (prev: z.infer) => { + console.log("prev", prev); + return { + ...prev, + userInfo: { + role: "guest", + nickname: "", + duck: 0, + message: "OK", + realDuck: prev.userInfo.realDuck, + }, + }; + }, + ); + + await queryClient.invalidateQueries({ + queryKey: authQueries.queryKey, + }); + + navigate({ to: "/login", replace: true }); + } catch (error) { + console.error("Logout Error:", error); + alert("로그아웃 중 오류가 발생했습니다. 다시 시도해 주세요."); + } finally { + setAuthState((prev) => ({ + ...prev, + isLoading: false, + })); + } + }, [navigate, setUserInfo, setAuthState, queryClient]); + + return logout; +}; diff --git a/frontend/src/shared/icons/DuckIcon.tsx b/frontend/src/shared/icons/DuckIcon.tsx index 540e3ac..4b15090 100644 --- a/frontend/src/shared/icons/DuckIcon.tsx +++ b/frontend/src/shared/icons/DuckIcon.tsx @@ -3,35 +3,22 @@ import React from "react"; const DuckIcon = React.memo(({ ...props }: React.SVGProps) => { return ( - - - - - ); diff --git a/frontend/src/shared/icons/LogoIcon.tsx b/frontend/src/shared/icons/LogoIcon.tsx index 367d3ff..aa93b9b 100644 --- a/frontend/src/shared/icons/LogoIcon.tsx +++ b/frontend/src/shared/icons/LogoIcon.tsx @@ -1,64 +1,28 @@ -function LogoIcon() { +import { memo } from "react"; + +const LogoIcon = memo(() => { return ( - - - - - - - - - - - - + + + @@ -66,85 +30,52 @@ function LogoIcon() { - - - - - - - - - - - - ); -} +}); export { LogoIcon }; diff --git a/frontend/src/shared/icons/PeoplesIcon.tsx b/frontend/src/shared/icons/PeoplesIcon.tsx index e81137f..9475bca 100644 --- a/frontend/src/shared/icons/PeoplesIcon.tsx +++ b/frontend/src/shared/icons/PeoplesIcon.tsx @@ -1,65 +1,41 @@ -function PeoplesIcon({ ...props }: React.SVGProps) { +import { memo } from "react"; + +const PeoplesIcon = memo(({ ...props }: React.SVGProps) => { return ( - - - + + - - - + + - - + + ); -} +}); export { PeoplesIcon }; diff --git a/frontend/src/shared/lib/auth/authQuery.ts b/frontend/src/shared/lib/auth/authQuery.ts index edef892..0af4d31 100644 --- a/frontend/src/shared/lib/auth/authQuery.ts +++ b/frontend/src/shared/lib/auth/authQuery.ts @@ -1,7 +1,8 @@ -import { checkAuthStatus } from "./guard"; +import { checkAuthStatus, getUserInfo } from "./guard"; import { QueryFunction } from "@tanstack/react-query"; import { AuthStatusTypeSchema } from "./guard"; import { z } from "zod"; +import { responseUserInfoSchema } from "@betting-duck/shared"; const authQueries = { queryKey: ["auth"], @@ -12,4 +13,9 @@ const authQueries = { staleTime: 1000 * 60 * 60, }; -export { authQueries }; +const userInfoQueries = { + queryKey: ["auth", "userInfos"], + queryFn: getUserInfo as QueryFunction>, +}; + +export { authQueries, userInfoQueries }; diff --git a/frontend/src/shared/lib/auth/guard.ts b/frontend/src/shared/lib/auth/guard.ts index 974e3cc..a7a6f0f 100644 --- a/frontend/src/shared/lib/auth/guard.ts +++ b/frontend/src/shared/lib/auth/guard.ts @@ -1,61 +1,6 @@ import { responseUserInfoSchema } from "@betting-duck/shared"; -import { redirect } from "@tanstack/react-router"; import { z } from "zod"; -export async function requireAuth({ to }: { to: string }) { - try { - const [tokenResponse, userInfoResponse] = await Promise.all([ - fetch("/api/users/token", { credentials: "include" }), - fetch("/api/users/userInfo", { credentials: "include" }), - ]); - - if (!tokenResponse.ok) { - throw redirect({ - to: "/require-login", - search: { from: encodeURIComponent(to) }, - }); - } - - if (!userInfoResponse.ok) { - throw new Error("사용자 정보를 불러오는데 실패했습니다."); - } - - const { data } = await userInfoResponse.json(); - const userInfo = responseUserInfoSchema.safeParse(data); - - if (!userInfo.success) { - throw new Error("사용자 정보를 파싱하는데 실패했습니다."); - } - - return userInfo.data; - } catch (error) { - console.error(error); - throw redirect({ - to: "/require-login", - search: { from: encodeURIComponent(to) }, - }); - } -} - -export async function requireUesrRole({ - userInfo, - to, - role, -}: { - userInfo: z.infer; - to: string; - role: "user" | "admin" | "guest"; -}) { - if (role === "guest") { - throw redirect({ - to: "/require-login", - search: { from: encodeURIComponent(to) }, - }); - } - - return userInfo; -} - export type UserInfoType = z.infer; type AuthStatusType = { @@ -77,45 +22,71 @@ export const AuthStatusTypeSchema = z.object({ }); export async function checkAuthStatus(): Promise { - try { - const [tokenResponse, userInfoResponse] = await Promise.all([ - fetch("/api/users/token", { - headers: { - "Cache-Control": "stale-while-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - }), - fetch("/api/users/userInfo", { - credentials: "include", - }), - ]); - - if (!tokenResponse.ok || !userInfoResponse.ok) { - return { - isAuthenticated: false, - userInfo: defaultUserInfo, - }; - } - - const { data } = await userInfoResponse.json(); - const result = responseUserInfoSchema.safeParse(data); - - if (!result.success) { - return { - isAuthenticated: false, - userInfo: defaultUserInfo, - }; - } + const tokenResponse = await fetch("/api/users/token", { + headers: { + "Cache-Control": "stale-while-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + }); + + if (!tokenResponse.ok) { + return { + isAuthenticated: false, + userInfo: defaultUserInfo, + }; + } + + const userInfoResponse = await fetch("/api/users/userInfo", { + headers: { + "Cache-Control": "stale-while-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + }); + if (!userInfoResponse.ok) { return { - isAuthenticated: true, - userInfo: result.data, + isAuthenticated: false, + userInfo: defaultUserInfo, }; - } catch { + } + + const { data } = await userInfoResponse.json(); + const result = responseUserInfoSchema.safeParse(data); + + if (!result.success) { return { isAuthenticated: false, userInfo: defaultUserInfo, }; } + + return { + isAuthenticated: true, + userInfo: result.data, + }; +} + +export async function getUserInfo() { + const userInfoResponse = await fetch("/api/users/userInfo", { + headers: { + "Cache-Control": "stale-while-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + }); + + if (!userInfoResponse.ok) { + return defaultUserInfo; + } + + const { data } = await userInfoResponse.json(); + const result = responseUserInfoSchema.safeParse(data); + + if (!result.success) { + return defaultUserInfo; + } + + return result.data; } diff --git a/frontend/src/shared/lib/bettingRoomInfo.ts b/frontend/src/shared/lib/bettingRoomInfo.ts index f56707f..9d5ae9b 100644 --- a/frontend/src/shared/lib/bettingRoomInfo.ts +++ b/frontend/src/shared/lib/bettingRoomInfo.ts @@ -1,5 +1,5 @@ import { useSuspenseQuery } from "@tanstack/react-query"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; export const bettingRoomQueryKey = (roomId: string) => ["bettingRoom", roomId]; diff --git a/frontend/src/shared/lib/updateQueryClient.ts b/frontend/src/shared/lib/updateQueryClient.ts new file mode 100644 index 0000000..d8fc233 --- /dev/null +++ b/frontend/src/shared/lib/updateQueryClient.ts @@ -0,0 +1,17 @@ +import { QueryClient } from "@tanstack/react-query"; +import { authQueries } from "../lib/auth/authQuery"; +import { z } from "zod"; +import { AuthStatusTypeSchema } from "../lib/auth/guard"; + +type QueryData = z.infer; + +async function updateQueryClient( + queryClient: QueryClient, + queryKey: string[], + callback: (prev: QueryData) => QueryData, +) { + queryClient.setQueryData(authQueries.queryKey, callback); + await queryClient.invalidateQueries({ queryKey }); +} + +export { updateQueryClient }; diff --git a/frontend/src/shared/lib/validateAccess.ts b/frontend/src/shared/lib/validateAccess.ts index 96f972e..a2baa04 100644 --- a/frontend/src/shared/lib/validateAccess.ts +++ b/frontend/src/shared/lib/validateAccess.ts @@ -1,4 +1,4 @@ -import { AccessError } from "@/features/waiting-room/error/AccessError"; +import { AccessError } from "@/pages/waiting-room/error/AccessError"; async function validateAccess(roomId: string, signal: AbortSignal) { try { diff --git a/frontend/src/widgets/Devtool/index.ts b/frontend/src/widgets/Devtool/index.ts new file mode 100644 index 0000000..26cf53e --- /dev/null +++ b/frontend/src/widgets/Devtool/index.ts @@ -0,0 +1 @@ +export { TanStackRouterDevtools } from "./ui/Devtool"; diff --git a/frontend/src/widgets/Devtool/ui/Devtool.tsx b/frontend/src/widgets/Devtool/ui/Devtool.tsx new file mode 100644 index 0000000..3ec56e2 --- /dev/null +++ b/frontend/src/widgets/Devtool/ui/Devtool.tsx @@ -0,0 +1,15 @@ +import { lazy } from "react"; + +const TanStackRouterDevtools = + process.env.NODE_ENV === "production" + ? () => null // Render nothing in production + : lazy(() => + // Lazy load in development + import("@tanstack/router-devtools").then((res) => ({ + default: res.TanStackRouterDevtools, + // For Embedded Mode + // default: res.TanStackRouterDevtoolsPanel + })), + ); + +export { TanStackRouterDevtools }; diff --git a/frontend/stats.html b/frontend/stats.html new file mode 100644 index 0000000..76bce28 --- /dev/null +++ b/frontend/stats.html @@ -0,0 +1,4949 @@ + + + + + + + + Rollup Visualizer + + + +
+ + + + + diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index ca415da..afef710 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -2,7 +2,7 @@ import plugin from "tailwindcss/plugin"; /** @type {import('tailwindcss').Config} */ export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}", "./public/**/*.html"], theme: { extend: { maxWidth: { diff --git a/frontend/tests/e2e/auth-redirect.spec.ts b/frontend/tests/e2e/auth-redirect.spec.ts new file mode 100644 index 0000000..c12f940 --- /dev/null +++ b/frontend/tests/e2e/auth-redirect.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from "@playwright/test"; + +test("has title", async ({ page }) => { + await page.goto("http://localhost:3000"); + await expect(page).toHaveTitle(/Betting duck/); +}); + +test("권한이 없을 경우 로그인 페이지로 리다이렉트", async ({ page }) => { + await page.goto("http://localhost:3000"); + + await page.getByRole("link", { name: /my/ }).click(); + await expect(page).toHaveURL(/require-login/); + + await page.getByRole("link", { name: /create vote/ }).click(); + await expect(page).toHaveURL(/require-login/); + + await page.getByRole("link", { name: /betting/ }).click(); + await expect(page).toHaveURL(/require-bettingRoomId/); +}); + +test("로그인 후 마이페이지로 리다이렉트", async ({ page }) => { + await page.goto("http://localhost:3000"); + + await page.getByPlaceholder("이메일을 입력해주세요.").fill("abc@naver.com"); + await page.getByPlaceholder("비밀번호를 입력해주세요.").fill("abc1234"); + + await page + .getByRole("button", { name: /로그인/ }) + .last() + .click(); + + await expect(page).toHaveURL(/my-page/); +}); + +test("쿠키 유지 상태에서 탭을 닫았다가 다시 열었을 때 인증 상태 유지 확인", async ({ + browser, +}) => { + const context = await browser.newContext({ acceptDownloads: true }); + const page = await context.newPage(); + + await page.goto("http://localhost:3000"); + + await page.getByPlaceholder("이메일을 입력해주세요.").fill("abc@naver.com"); + await page.getByPlaceholder("비밀번호를 입력해주세요.").fill("abc1234"); + + await page + .getByRole("button", { name: /로그인/ }) + .last() + .click(); + await expect(page).toHaveURL(/my-page/); + + await page.close(); + + const newPage = await context.newPage(); + await newPage.goto("http://localhost:3000"); + + await expect(newPage).not.toHaveURL(/login/); + await expect(newPage).toHaveURL(/my-page/); +}); diff --git a/frontend/test/sum.test.ts b/frontend/tests/sum.test.ts similarity index 100% rename from frontend/test/sum.test.ts rename to frontend/tests/sum.test.ts diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7cbe3bd..486a738 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,6 +3,9 @@ import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; import tsconfigPaths from "vite-tsconfig-paths"; +import { visualizer } from "rollup-plugin-visualizer"; +import { compression } from "vite-plugin-compression2"; +import { ViteImageOptimizer } from "vite-plugin-image-optimizer"; // https://vite.dev/config/ export default defineConfig(({ mode }) => { @@ -18,7 +21,21 @@ export default defineConfig(({ mode }) => { }, }, assetsInclude: ["**/*.glb", "**/*.hdr"], - plugins: [react(), TanStackRouterVite(), tsconfigPaths()], + plugins: [ + react(), + TanStackRouterVite(), + tsconfigPaths(), + visualizer(), + compression(), + ViteImageOptimizer({ + png: { + quality: 70, + }, + avif: { + quality: 70, + }, + }), + ], server: { port: 3000, proxy: { @@ -50,9 +67,16 @@ export default defineConfig(({ mode }) => { }, build: { target: "esnext", - sourcemap: true, - modulePreload: { - polyfill: true, + minify: "terser", + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + mangle: true, + format: { + comments: false, + }, }, chunkSizeWarningLimit: 1000, assetsDir: "assets", @@ -84,14 +108,17 @@ export default defineConfig(({ mode }) => { manualChunks(id) { if (id.includes("node_modules")) { if (id.includes("@tanstack")) return "vendor-tanstack"; - if (id.includes("react")) return "vendor-react"; + if (id.includes("@react-three/cannon")) + return "vendor-react-three-cannon"; + if (id.includes("@react-three/drei")) + return "vendor-react-three-drei"; + if (id.includes("@react-three/fiber")) + return "vendor-react-three-fiber"; + if (id.includes("three")) return "vendor-three"; if (id.includes("@socket")) return "vendor-socket"; - return "vendor"; - } + if (id.includes("react")) return "vendor-react"; - if (id.includes("/features/")) { - const feature = id.split("/features/")[1].split("/")[0]; - return `feature-${feature}`; + return "vendor"; } }, }, @@ -100,7 +127,7 @@ export default defineConfig(({ mode }) => { }, optimizeDeps: { include: ["react", "react-dom"], - exclude: ["@tanstack/router"], + exclude: ["@tanstack/router", "three"], }, }; }); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..0cae3d5 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + // Since hooks are running in stack in v2, which means all hooks run serially whereas + // we need to run them in parallel + sequence: { + hooks: "parallel", + }, + setupFiles: ["./setupTests.ts"], + globals: true, + environment: "jsdom", + coverage: { + reporter: ["text", "json-summary", "json", "html", "lcovonly"], + // Since v2, it ignores empty lines by default and we need to disable it as it affects the coverage + // Additionally the thresholds also needs to be updated slightly as a result of this change + ignoreEmptyLines: false, + thresholds: { + lines: 60, + branches: 70, + functions: 63, + statements: 60, + }, + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca83fc5..7a1b4de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,13 +225,13 @@ importers: version: 1.1.0(@types/react@18.3.12)(react@18.3.1) '@react-three/cannon': specifier: ^6.6.0 - version: 6.6.0(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0))(react@18.3.1)(three@0.171.0)(typescript@5.6.3) + version: 6.6.0(@react-three/fiber@8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0))(react@18.3.1)(three@0.172.0)(typescript@5.6.3) '@react-three/drei': specifier: ^9.119.0 - version: 9.119.0(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0))(@types/react@18.3.12)(@types/three@0.170.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0)(use-sync-external-store@1.2.2(react@18.3.1)) + version: 9.119.0(@react-three/fiber@8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0))(@types/react@18.3.12)(@types/three@0.170.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0)(use-sync-external-store@1.2.2(react@18.3.1)) '@react-three/fiber': - specifier: ^8.17.10 - version: 8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0) + specifier: ^8.17.12 + version: 8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0) '@tanstack/react-query': specifier: ^5.62.0 version: 5.62.0(react@18.3.1) @@ -256,28 +256,43 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + recoil: + specifier: ^0.7.7 + version: 0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) socket.io-client: specifier: ^4.8.1 version: 4.8.1 tailwind-merge: specifier: ^2.5.4 version: 2.5.4 + terser: + specifier: ^5.37.0 + version: 5.37.0 three: - specifier: ^0.171.0 - version: 0.171.0 + specifier: ^0.172.0 + version: 0.172.0 devDependencies: '@eslint/js': specifier: ^9.13.0 version: 9.14.0 + '@playwright/test': + specifier: ^1.49.1 + version: 1.49.1 '@tanstack/router-cli': specifier: ^1.79.0 version: 1.79.0 '@tanstack/router-devtools': - specifier: ^1.81.1 + specifier: ^1.81.5 version: 1.81.5(@tanstack/react-router@1.81.5(@tanstack/router-generator@1.79.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-plugin': specifier: ^1.79.0 - version: 1.79.0(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0))(webpack@5.96.1) + version: 1.79.0(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0))(webpack@5.96.1) + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.0 + '@testing-library/react': + specifier: ^16.2.0 + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/node': specifier: ^22.9.0 version: 22.9.0 @@ -289,10 +304,13 @@ importers: version: 18.3.1 '@vitejs/plugin-react': specifier: ^4.3.3 - version: 4.3.3(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)) + version: 4.3.3(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0)) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) + cssnano: + specifier: ^7.0.6 + version: 7.0.6(postcss@8.4.49) eslint: specifier: ^9.13.0 version: 9.14.0(jiti@1.21.6) @@ -305,6 +323,9 @@ importers: globals: specifier: ^15.11.0 version: 15.12.0 + jsdom: + specifier: ^26.0.0 + version: 26.0.0 msw: specifier: ^2.6.4 version: 2.6.4(@types/node@22.9.0)(typescript@5.6.3) @@ -314,6 +335,12 @@ importers: prettier: specifier: ^3.3.3 version: 3.3.3 + rollup-plugin-visualizer: + specifier: ^5.14.0 + version: 5.14.0(rollup@4.26.0) + sharp: + specifier: ^0.33.5 + version: 0.33.5 tailwindcss: specifier: ^3.4.14 version: 3.4.14(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) @@ -325,13 +352,19 @@ importers: version: 8.14.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) vite: specifier: ^5.4.10 - version: 5.4.11(@types/node@22.9.0)(terser@5.36.0) + version: 5.4.11(@types/node@22.9.0)(terser@5.37.0) + vite-plugin-compression2: + specifier: ^1.3.3 + version: 1.3.3(rollup@4.26.0)(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0)) + vite-plugin-image-optimizer: + specifier: ^1.1.8 + version: 1.1.8(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0)) vite-tsconfig-paths: specifier: ^5.1.3 - version: 5.1.3(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)) + version: 5.1.3(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0)) vitest: - specifier: ^2.1.4 - version: 2.1.5(@types/node@22.9.0)(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(terser@5.36.0) + specifier: ^2.1.5 + version: 2.1.5(@types/node@22.9.0)(jsdom@26.0.0)(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(terser@5.37.0) shared: dependencies: @@ -367,6 +400,9 @@ packages: resolution: {integrity: sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==} engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@asamuzakjp/css-color@2.8.3': + resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -561,6 +597,37 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/color-helpers@5.0.1': + resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.1': + resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-color-parser@3.0.7': + resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -951,6 +1018,111 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/confirm@5.0.2': resolution: {integrity: sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==} engines: {node: '>=18'} @@ -1421,6 +1593,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.49.1': + resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + engines: {node: '>=18'} + hasBin: true + '@pmndrs/cannon-worker-api@2.4.0': resolution: {integrity: sha512-oJA1Bboc+WObksRaGDKJG0Wna9Q75xi1MdXVAZ9qXzBOyPsadmAnrmiKOEF0R8v/4zsuJElvscNZmyo3msbZjA==} peerDependencies: @@ -1493,15 +1670,15 @@ packages: react-dom: optional: true - '@react-three/fiber@8.17.10': - resolution: {integrity: sha512-S6bqa4DqUooEkInYv/W+Jklv2zjSYCXAhm6qKpAQyOXhTEt5gBXnA7W6aoJ0bjmp9pAeaSj/AZUoz1HCSof/uA==} + '@react-three/fiber@8.17.12': + resolution: {integrity: sha512-rjV/ZtCr69y+aWEOsAhBQzsxYyvZHUanYfo9eMXNp/dxTj3ZrRvK44DkIdSLV1xcPidq8p2YeU2oWP2czY+ZVA==} peerDependencies: expo: '>=43.0' expo-asset: '>=8.4' expo-file-system: '>=11.0' expo-gl: '>=11.0' - react: '>=18.0' - react-dom: '>=18.0' + react: '>=18 <19' + react-dom: '>=18 <19' react-native: '>=0.64' three: '>=0.133' peerDependenciesMeta: @@ -1518,6 +1695,15 @@ packages: react-native: optional: true + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.26.0': resolution: {integrity: sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==} cpu: [arm] @@ -1703,6 +1889,29 @@ packages: resolution: {integrity: sha512-soW+gE9QTmMaqXM17r7y1p8NiQVIIECjdTaYla8BKL5Flj030m3KuxEQoiG1XgjtA0O7ayznFz2YvPcXIy3qDg==} engines: {node: '>=12'} + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/react@16.2.0': + resolution: {integrity: sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -1718,6 +1927,9 @@ packages: '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2049,6 +2261,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -2126,6 +2342,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -2239,6 +2458,9 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2310,6 +2532,9 @@ packages: peerDependencies: three: '>=0.126.1' + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + caniuse-lite@1.0.30001680: resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} @@ -2430,6 +2655,16 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -2448,6 +2683,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + comment-json@4.2.5: resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} engines: {node: '>= 6'} @@ -2534,14 +2773,65 @@ packages: resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} engines: {node: '>= 8'} + css-declaration-sorter@7.2.0: + resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.0.9 + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + cssnano-preset-default@7.0.6: + resolution: {integrity: sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-utils@5.0.0: + resolution: {integrity: sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano@7.0.6: + resolution: {integrity: sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + cssstyle@4.2.1: + resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} + engines: {node: '>=18'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -2585,6 +2875,9 @@ packages: supports-color: optional: true + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -2611,6 +2904,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -2627,6 +2924,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -2634,6 +2935,10 @@ packages: detect-gpu@5.0.59: resolution: {integrity: sha512-tBS01N6Lu7D2T6nmoA5or39u12PqtmO4RPCSfgvbgcOUGPhExU8RDiyUiT3iVxCKX9XTxIM1k+mVaeirLv6pwA==} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -2659,9 +2964,25 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} @@ -2726,6 +3047,10 @@ packages: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2908,6 +3233,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -3078,6 +3406,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3192,6 +3525,9 @@ packages: resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + hamt_plus@1.0.2: + resolution: {integrity: sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==} + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -3238,6 +3574,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3245,6 +3585,14 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3262,6 +3610,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3319,6 +3671,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -3346,6 +3701,11 @@ packages: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3389,6 +3749,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} @@ -3427,6 +3790,10 @@ packages: is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -3625,6 +3992,15 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@26.0.0: + resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -3756,6 +4132,9 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -3780,6 +4159,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + maath@0.10.8: resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} peerDependencies: @@ -3803,6 +4186,12 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -3998,6 +4387,12 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4049,6 +4444,10 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4104,6 +4503,9 @@ packages: parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4210,6 +4612,10 @@ packages: resolution: {integrity: sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==} engines: {node: '>=12'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -4227,6 +4633,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -4235,6 +4651,48 @@ packages: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} + postcss-calc@10.1.0: + resolution: {integrity: sha512-uQ/LDGsf3mgsSUEXmAt3VsCSHR3aKqtEIkmB+4PhzYwRYOW5MZs/GhCCFpsOtJJkP6EC6uGipbrnaTjqaJZcJw==} + engines: {node: ^18.12 || ^20.9 || >=22.0} + peerDependencies: + postcss: ^8.4.38 + + postcss-colormin@7.0.2: + resolution: {integrity: sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-convert-values@7.0.4: + resolution: {integrity: sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-comments@7.0.3: + resolution: {integrity: sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-duplicates@7.0.1: + resolution: {integrity: sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-empty@7.0.0: + resolution: {integrity: sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-overridden@7.0.0: + resolution: {integrity: sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -4259,16 +4717,140 @@ packages: ts-node: optional: true + postcss-merge-longhand@7.0.4: + resolution: {integrity: sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-rules@7.0.4: + resolution: {integrity: sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-font-values@7.0.0: + resolution: {integrity: sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-gradients@7.0.0: + resolution: {integrity: sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-params@7.0.2: + resolution: {integrity: sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-selectors@7.0.4: + resolution: {integrity: sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + postcss-nested@6.2.0: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 + postcss-normalize-charset@7.0.0: + resolution: {integrity: sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-display-values@7.0.0: + resolution: {integrity: sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-positions@7.0.0: + resolution: {integrity: sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-repeat-style@7.0.0: + resolution: {integrity: sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-string@7.0.0: + resolution: {integrity: sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-timing-functions@7.0.0: + resolution: {integrity: sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-unicode@7.0.2: + resolution: {integrity: sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-url@7.0.0: + resolution: {integrity: sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-whitespace@7.0.0: + resolution: {integrity: sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-ordered-values@7.0.1: + resolution: {integrity: sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-initial@7.0.2: + resolution: {integrity: sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-transforms@7.0.0: + resolution: {integrity: sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} + postcss-selector-parser@7.0.0: + resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==} + engines: {node: '>=4'} + + postcss-svgo@7.0.1: + resolution: {integrity: sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==} + engines: {node: ^18.12.0 || ^20.9.0 || >= 18} + peerDependencies: + postcss: ^8.4.31 + + postcss-unique-selectors@7.0.3: + resolution: {integrity: sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -4363,6 +4945,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4428,6 +5014,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -4465,6 +5054,18 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + recoil@0.7.7: + resolution: {integrity: sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==} + peerDependencies: + react: '>=16.13.1' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -4536,11 +5137,27 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup-plugin-visualizer@5.14.0: + resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + rolldown: 1.x + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + rollup@4.26.0: resolution: {integrity: sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -4572,6 +5189,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.21.0: resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} @@ -4617,6 +5238,10 @@ packages: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} hasBin: true + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -4639,6 +5264,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -4800,6 +5428,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + stylehacks@7.0.4: + resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -4833,6 +5467,11 @@ packages: peerDependencies: react: '>=17.0' + svgo@3.3.2: + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} + hasBin: true + swagger-ui-dist@5.18.2: resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==} @@ -4840,6 +5479,9 @@ packages: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.9.2: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4856,6 +5498,9 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + tar-mini@0.2.0: + resolution: {integrity: sha512-+qfUHz700DWnRutdUsxRRVZ38G1Qr27OetwaMYTdg8hcPxf46U0S1Zf76dQMWRBmusOt2ZCK5kbIaiLkoGO7WQ==} + terser-webpack-plugin@5.3.10: resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} @@ -4877,6 +5522,11 @@ packages: engines: {node: '>=10'} hasBin: true + terser@5.37.0: + resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==} + engines: {node: '>=10'} + hasBin: true + test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -4902,8 +5552,8 @@ packages: peerDependencies: three: '>=0.128.0' - three@0.171.0: - resolution: {integrity: sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==} + three@0.172.0: + resolution: {integrity: sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==} through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -4932,6 +5582,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.72: + resolution: {integrity: sha512-FW3H9aCaGTJ8l8RVCR3EX8GxsxDbQXuwetwwgXA2chYdsX+NY1ytCBl61narjjehWmCw92tc1AxlcY3668CU8g==} + + tldts@6.1.72: + resolution: {integrity: sha512-QNtgIqSUb9o2CoUjX9T5TwaIvUUJFU1+12PJkgt42DFV2yf9J6549yTF2uGloQsJ/JOC8X+gIB81ind97hRiIQ==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -4951,9 +5608,17 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tough-cookie@5.1.0: + resolution: {integrity: sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -5253,6 +5918,17 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-plugin-compression2@1.3.3: + resolution: {integrity: sha512-Mb+xi/C5b68awtF4fNwRBPtoZiyUHU3I0SaBOAGlerlR31kusq1si6qG31lsjJH8T7QNg/p3IJY2HY9O9SvsfQ==} + peerDependencies: + vite: ^2.0.0||^3.0.0||^4.0.0||^5.0.0 ||^6.0.0 + + vite-plugin-image-optimizer@1.1.8: + resolution: {integrity: sha512-40bYRDHQLUOrIwJIJQqyKJHrfgVshqzDLtMy8SEgf+fB7PnppslSTTkY7PJFrBGqgbCdOdN9KkqsvccXmnEa5Q==} + engines: {node: '>=14'} + peerDependencies: + vite: '>=3' + vite-tsconfig-paths@5.1.3: resolution: {integrity: sha512-0bz+PDlLpGfP2CigeSKL9NFTF1KtXkeHGZSSaGQSuPZH77GhoiQaA8IjYgOaynSuwlDTolSUEU0ErVvju3NURg==} peerDependencies: @@ -5317,6 +5993,10 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -5336,6 +6016,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -5357,6 +6041,18 @@ packages: webpack-cli: optional: true + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.1.0: + resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -5416,6 +6112,25 @@ packages: utf-8-validate: optional: true + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlhttprequest-ssl@2.1.2: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} @@ -5559,6 +6274,14 @@ snapshots: transitivePeerDependencies: - chokidar + '@asamuzakjp/css-color@2.8.3': + dependencies: + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 10.4.3 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -5780,6 +6503,31 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.0.1': {} + + '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/color-helpers': 5.0.1 + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.25.9 @@ -6056,6 +6804,81 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@inquirer/confirm@5.0.2(@types/node@22.9.0)': dependencies: '@inquirer/core': 10.1.0(@types/node@22.9.0) @@ -6314,10 +7137,10 @@ snapshots: '@microsoft/tsdoc@0.15.0': {} - '@monogrid/gainmap-js@3.0.6(three@0.171.0)': + '@monogrid/gainmap-js@3.0.6(three@0.172.0)': dependencies: promise-worker-transferable: 1.0.4 - three: 0.171.0 + three: 0.172.0 '@mswjs/interceptors@0.36.10': dependencies: @@ -6612,9 +7435,13 @@ snapshots: '@pkgr/core@0.1.1': {} - '@pmndrs/cannon-worker-api@2.4.0(three@0.171.0)': + '@playwright/test@1.49.1': dependencies: - three: 0.171.0 + playwright: 1.49.1 + + '@pmndrs/cannon-worker-api@2.4.0(three@0.172.0)': + dependencies: + three: 0.172.0 '@popperjs/core@2.11.8': {} @@ -6652,53 +7479,53 @@ snapshots: '@react-spring/types': 9.7.5 react: 18.3.1 - '@react-spring/three@9.7.5(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0))(react@18.3.1)(three@0.171.0)': + '@react-spring/three@9.7.5(@react-three/fiber@8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0))(react@18.3.1)(three@0.172.0)': dependencies: '@react-spring/animated': 9.7.5(react@18.3.1) '@react-spring/core': 9.7.5(react@18.3.1) '@react-spring/shared': 9.7.5(react@18.3.1) '@react-spring/types': 9.7.5 - '@react-three/fiber': 8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0) + '@react-three/fiber': 8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0) react: 18.3.1 - three: 0.171.0 + three: 0.172.0 '@react-spring/types@9.7.5': {} - '@react-three/cannon@6.6.0(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0))(react@18.3.1)(three@0.171.0)(typescript@5.6.3)': + '@react-three/cannon@6.6.0(@react-three/fiber@8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0))(react@18.3.1)(three@0.172.0)(typescript@5.6.3)': dependencies: - '@pmndrs/cannon-worker-api': 2.4.0(three@0.171.0) - '@react-three/fiber': 8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0) + '@pmndrs/cannon-worker-api': 2.4.0(three@0.172.0) + '@react-three/fiber': 8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0) cannon-es: 0.20.0 - cannon-es-debugger: 1.0.0(cannon-es@0.20.0)(three@0.171.0)(typescript@5.6.3) + cannon-es-debugger: 1.0.0(cannon-es@0.20.0)(three@0.172.0)(typescript@5.6.3) react: 18.3.1 - three: 0.171.0 + three: 0.172.0 transitivePeerDependencies: - typescript - '@react-three/drei@9.119.0(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0))(@types/react@18.3.12)(@types/three@0.170.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0)(use-sync-external-store@1.2.2(react@18.3.1))': + '@react-three/drei@9.119.0(@react-three/fiber@8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0))(@types/react@18.3.12)(@types/three@0.170.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0)(use-sync-external-store@1.2.2(react@18.3.1))': dependencies: '@babel/runtime': 7.26.0 '@mediapipe/tasks-vision': 0.10.17 - '@monogrid/gainmap-js': 3.0.6(three@0.171.0) - '@react-spring/three': 9.7.5(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0))(react@18.3.1)(three@0.171.0) - '@react-three/fiber': 8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0) + '@monogrid/gainmap-js': 3.0.6(three@0.172.0) + '@react-spring/three': 9.7.5(@react-three/fiber@8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0))(react@18.3.1)(three@0.172.0) + '@react-three/fiber': 8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0) '@use-gesture/react': 10.3.1(react@18.3.1) - camera-controls: 2.9.0(three@0.171.0) + camera-controls: 2.9.0(three@0.172.0) cross-env: 7.0.3 detect-gpu: 5.0.59 glsl-noise: 0.0.0 hls.js: 1.5.17 - maath: 0.10.8(@types/three@0.170.0)(three@0.171.0) - meshline: 3.3.1(three@0.171.0) + maath: 0.10.8(@types/three@0.170.0)(three@0.172.0) + meshline: 3.3.1(three@0.172.0) react: 18.3.1 react-composer: 5.0.3(react@18.3.1) - stats-gl: 2.4.2(@types/three@0.170.0)(three@0.171.0) + stats-gl: 2.4.2(@types/three@0.170.0)(three@0.172.0) stats.js: 0.17.0 suspend-react: 0.1.3(react@18.3.1) - three: 0.171.0 - three-mesh-bvh: 0.7.8(three@0.171.0) - three-stdlib: 2.34.0(three@0.171.0) - troika-three-text: 0.52.2(three@0.171.0) + three: 0.172.0 + three-mesh-bvh: 0.7.8(three@0.172.0) + three-stdlib: 2.34.0(three@0.172.0) + troika-three-text: 0.52.2(three@0.172.0) tunnel-rat: 0.1.2(@types/react@18.3.12)(react@18.3.1) utility-types: 3.11.0 uuid: 9.0.1 @@ -6711,7 +7538,7 @@ snapshots: - immer - use-sync-external-store - '@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.171.0)': + '@react-three/fiber@8.17.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.172.0)': dependencies: '@babel/runtime': 7.26.0 '@types/debounce': 1.2.4 @@ -6725,11 +7552,19 @@ snapshots: react-reconciler: 0.27.0(react@18.3.1) scheduler: 0.21.0 suspend-react: 0.1.3(react@18.3.1) - three: 0.171.0 + three: 0.172.0 zustand: 3.7.2(react@18.3.1) optionalDependencies: react-dom: 18.3.1(react@18.3.1) + '@rollup/pluginutils@5.1.4(rollup@4.26.0)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.26.0 + '@rollup/rollup-android-arm-eabi@4.26.0': optional: true @@ -6862,7 +7697,7 @@ snapshots: tsx: 4.19.2 zod: 3.23.8 - '@tanstack/router-plugin@1.79.0(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0))(webpack@5.96.1)': + '@tanstack/router-plugin@1.79.0(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0))(webpack@5.96.1)': dependencies: '@babel/core': 7.26.0 '@babel/generator': 7.26.2 @@ -6883,7 +7718,7 @@ snapshots: unplugin: 1.16.0 zod: 3.23.8 optionalDependencies: - vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.9.0)(terser@5.37.0) webpack: 5.96.1 transitivePeerDependencies: - supports-color @@ -6892,6 +7727,29 @@ snapshots: '@tanstack/virtual-file-routes@1.64.0': {} + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/runtime': 7.26.0 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@testing-library/dom': 10.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@trysound/sax@0.2.0': {} + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -6902,6 +7760,8 @@ snapshots: '@tweenjs/tween.js@23.1.3': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.2 @@ -7182,14 +8042,14 @@ snapshots: '@use-gesture/core': 10.3.1 react: 18.3.1 - '@vitejs/plugin-react@4.3.3(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0))': + '@vitejs/plugin-react@4.3.3(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.9.0)(terser@5.37.0) transitivePeerDependencies: - supports-color @@ -7200,14 +8060,14 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.5(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0))': + '@vitest/mocker@2.1.5(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0))': dependencies: '@vitest/spy': 2.1.5 estree-walker: 3.0.3 magic-string: 0.30.12 optionalDependencies: msw: 2.6.4(@types/node@22.9.0)(typescript@5.6.3) - vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.9.0)(terser@5.37.0) '@vitest/pretty-format@2.1.5': dependencies: @@ -7331,6 +8191,8 @@ snapshots: acorn@8.14.0: {} + agent-base@7.1.3: {} + ajv-formats@2.1.1(ajv@8.12.0): optionalDependencies: ajv: 8.12.0 @@ -7396,6 +8258,10 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + array-buffer-byte-length@1.0.1: dependencies: call-bind: 1.0.7 @@ -7577,6 +8443,8 @@ snapshots: transitivePeerDependencies: - supports-color + boolbase@1.0.0: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -7643,16 +8511,23 @@ snapshots: camelcase@6.3.0: {} - camera-controls@2.9.0(three@0.171.0): + camera-controls@2.9.0(three@0.172.0): dependencies: - three: 0.171.0 + three: 0.172.0 + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.24.2 + caniuse-lite: 1.0.30001680 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 caniuse-lite@1.0.30001680: {} - cannon-es-debugger@1.0.0(cannon-es@0.20.0)(three@0.171.0)(typescript@5.6.3): + cannon-es-debugger@1.0.0(cannon-es@0.20.0)(three@0.172.0)(typescript@5.6.3): dependencies: cannon-es: 0.20.0 - three: 0.171.0 + three: 0.172.0 optionalDependencies: typescript: 5.6.3 @@ -7759,6 +8634,18 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + colord@2.9.3: {} + colorette@2.0.20: {} combined-stream@1.0.8: @@ -7771,6 +8658,8 @@ snapshots: commander@4.1.1: {} + commander@7.2.0: {} + comment-json@4.2.5: dependencies: array-timsort: 1.0.3 @@ -7866,10 +8755,92 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-declaration-sorter@7.2.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + cssesc@3.0.0: {} + cssnano-preset-default@7.0.6(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + css-declaration-sorter: 7.2.0(postcss@8.4.49) + cssnano-utils: 5.0.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-calc: 10.1.0(postcss@8.4.49) + postcss-colormin: 7.0.2(postcss@8.4.49) + postcss-convert-values: 7.0.4(postcss@8.4.49) + postcss-discard-comments: 7.0.3(postcss@8.4.49) + postcss-discard-duplicates: 7.0.1(postcss@8.4.49) + postcss-discard-empty: 7.0.0(postcss@8.4.49) + postcss-discard-overridden: 7.0.0(postcss@8.4.49) + postcss-merge-longhand: 7.0.4(postcss@8.4.49) + postcss-merge-rules: 7.0.4(postcss@8.4.49) + postcss-minify-font-values: 7.0.0(postcss@8.4.49) + postcss-minify-gradients: 7.0.0(postcss@8.4.49) + postcss-minify-params: 7.0.2(postcss@8.4.49) + postcss-minify-selectors: 7.0.4(postcss@8.4.49) + postcss-normalize-charset: 7.0.0(postcss@8.4.49) + postcss-normalize-display-values: 7.0.0(postcss@8.4.49) + postcss-normalize-positions: 7.0.0(postcss@8.4.49) + postcss-normalize-repeat-style: 7.0.0(postcss@8.4.49) + postcss-normalize-string: 7.0.0(postcss@8.4.49) + postcss-normalize-timing-functions: 7.0.0(postcss@8.4.49) + postcss-normalize-unicode: 7.0.2(postcss@8.4.49) + postcss-normalize-url: 7.0.0(postcss@8.4.49) + postcss-normalize-whitespace: 7.0.0(postcss@8.4.49) + postcss-ordered-values: 7.0.1(postcss@8.4.49) + postcss-reduce-initial: 7.0.2(postcss@8.4.49) + postcss-reduce-transforms: 7.0.0(postcss@8.4.49) + postcss-svgo: 7.0.1(postcss@8.4.49) + postcss-unique-selectors: 7.0.3(postcss@8.4.49) + + cssnano-utils@5.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + cssnano@7.0.6(postcss@8.4.49): + dependencies: + cssnano-preset-default: 7.0.6(postcss@8.4.49) + lilconfig: 3.1.2 + postcss: 8.4.49 + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + cssstyle@4.2.1: + dependencies: + '@asamuzakjp/css-color': 2.8.3 + rrweb-cssom: 0.8.0 + csstype@3.1.3: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + data-view-buffer@1.0.1: dependencies: call-bind: 1.0.7 @@ -7904,6 +8875,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.4.3: {} + dedent@1.5.3(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -7924,6 +8897,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.0.1 + define-lazy-prop@2.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -7936,12 +8911,16 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-gpu@5.0.59: dependencies: webgl-constants: 1.1.1 + detect-libc@2.0.3: {} + detect-newline@3.1.0: {} dezalgo@1.0.4: @@ -7961,11 +8940,31 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.26.0 csstype: 3.1.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv-expand@10.0.0: {} dotenv@16.4.5: {} @@ -8034,6 +9033,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + entities@4.5.0: {} + environment@1.1.0: {} error-ex@1.3.2: @@ -8322,6 +9323,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 @@ -8551,6 +9554,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -8661,6 +9667,8 @@ snapshots: graphql@16.9.0: {} + hamt_plus@1.0.2: {} + has-bigints@1.0.2: {} has-flag@4.0.0: {} @@ -8695,6 +9703,10 @@ snapshots: dependencies: react-is: 16.13.1 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -8705,6 +9717,20 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -8715,6 +9741,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -8805,6 +9835,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-bigint@1.0.4: dependencies: has-bigints: 1.0.2 @@ -8832,6 +9864,8 @@ snapshots: dependencies: has-tostringtag: 1.0.2 + is-docker@2.2.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -8860,6 +9894,8 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@2.2.2: {} is-regex@1.1.4: @@ -8893,6 +9929,10 @@ snapshots: dependencies: call-bind: 1.0.7 + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -9288,6 +10328,34 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@26.0.0: + dependencies: + cssstyle: 4.2.1 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.2.1 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.0.2: {} json-buffer@3.0.1: {} @@ -9419,6 +10487,8 @@ snapshots: lodash.once@4.1.1: {} + lodash.uniq@4.5.0: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -9446,10 +10516,12 @@ snapshots: dependencies: yallist: 3.1.1 - maath@0.10.8(@types/three@0.170.0)(three@0.171.0): + lz-string@1.5.0: {} + + maath@0.10.8(@types/three@0.170.0)(three@0.172.0): dependencies: '@types/three': 0.170.0 - three: 0.171.0 + three: 0.172.0 magic-string@0.30.12: dependencies: @@ -9469,6 +10541,10 @@ snapshots: dependencies: tmpl: 1.0.5 + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + media-typer@0.3.0: {} memfs@3.5.3: @@ -9481,9 +10557,9 @@ snapshots: merge2@1.4.1: {} - meshline@3.3.1(three@0.171.0): + meshline@3.3.1(three@0.172.0): dependencies: - three: 0.171.0 + three: 0.172.0 meshoptimizer@0.18.1: {} @@ -9629,6 +10705,12 @@ snapshots: dependencies: path-key: 4.0.0 + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nwsapi@2.2.16: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -9683,6 +10765,12 @@ snapshots: dependencies: mimic-function: 5.0.1 + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -9747,6 +10835,10 @@ snapshots: parse5@6.0.1: {} + parse5@7.2.1: + dependencies: + entities: 4.5.0 + parseurl@1.3.3: {} passport-jwt@4.0.1: @@ -9832,6 +10924,8 @@ snapshots: picomatch@4.0.1: {} + picomatch@4.0.2: {} + pidtree@0.6.0: {} pify@2.3.0: {} @@ -9842,10 +10936,55 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.49.1: {} + + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.0.0: {} + postcss-calc@10.1.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-selector-parser: 7.0.0 + postcss-value-parser: 4.2.0 + + postcss-colormin@7.0.2(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-convert-values@7.0.4(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-discard-comments@7.0.3(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + + postcss-discard-duplicates@7.0.1(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + postcss-discard-empty@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + postcss-discard-overridden@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-import@15.1.0(postcss@8.4.49): dependencies: postcss: 8.4.49 @@ -9866,16 +11005,133 @@ snapshots: postcss: 8.4.49 ts-node: 10.9.2(@types/node@22.9.0)(typescript@5.6.3) + postcss-merge-longhand@7.0.4(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + stylehacks: 7.0.4(postcss@8.4.49) + + postcss-merge-rules@7.0.4(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + caniuse-api: 3.0.0 + cssnano-utils: 5.0.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + + postcss-minify-font-values@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@7.0.0(postcss@8.4.49): + dependencies: + colord: 2.9.3 + cssnano-utils: 5.0.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-minify-params@7.0.2(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + cssnano-utils: 5.0.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@7.0.4(postcss@8.4.49): + dependencies: + cssesc: 3.0.0 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + postcss-nested@6.2.0(postcss@8.4.49): dependencies: postcss: 8.4.49 postcss-selector-parser: 6.1.2 + postcss-normalize-charset@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + postcss-normalize-display-values@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@7.0.2(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-ordered-values@7.0.1(postcss@8.4.49): + dependencies: + cssnano-utils: 5.0.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + postcss-reduce-initial@7.0.2(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + caniuse-api: 3.0.0 + postcss: 8.4.49 + + postcss-reduce-transforms@7.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@7.0.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-svgo@7.0.1(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + svgo: 3.3.2 + + postcss-unique-selectors@7.0.3(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + postcss-value-parser@4.2.0: {} postcss@8.4.49: @@ -9908,6 +11164,12 @@ snapshots: prettier@3.3.3: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -9979,6 +11241,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-reconciler@0.27.0(react@18.3.1): @@ -10026,6 +11290,13 @@ snapshots: dependencies: picomatch: 2.3.1 + recoil@0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + hamt_plus: 1.0.2 + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -10083,6 +11354,15 @@ snapshots: rfdc@1.4.1: {} + rollup-plugin-visualizer@5.14.0(rollup@4.26.0): + dependencies: + open: 8.4.2 + picomatch: 4.0.2 + source-map: 0.7.4 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.26.0 + rollup@4.26.0: dependencies: '@types/estree': 1.0.6 @@ -10107,6 +11387,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.26.0 fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} + run-async@2.4.1: {} run-async@3.0.0: {} @@ -10138,6 +11420,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.21.0: dependencies: loose-envify: 1.4.0 @@ -10210,6 +11496,32 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -10229,6 +11541,10 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -10328,10 +11644,10 @@ snapshots: standard-as-callback@2.1.0: {} - stats-gl@2.4.2(@types/three@0.170.0)(three@0.171.0): + stats-gl@2.4.2(@types/three@0.170.0)(three@0.172.0): dependencies: '@types/three': 0.170.0 - three: 0.171.0 + three: 0.172.0 stats.js@0.17.0: {} @@ -10413,6 +11729,12 @@ snapshots: strip-json-comments@3.1.1: {} + stylehacks@7.0.4(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + stylis@4.2.0: {} sucrase@3.35.0: @@ -10460,12 +11782,24 @@ snapshots: dependencies: react: 18.3.1 + svgo@3.3.2: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.1.1 + swagger-ui-dist@5.18.2: dependencies: '@scarf/scarf': 1.4.0 symbol-observable@4.0.0: {} + symbol-tree@3.2.4: {} + synckit@0.9.2: dependencies: '@pkgr/core': 0.1.1 @@ -10502,6 +11836,8 @@ snapshots: tapable@2.2.1: {} + tar-mini@0.2.0: {} + terser-webpack-plugin@5.3.10(webpack@5.96.1): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -10518,6 +11854,13 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + terser@5.37.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.3 @@ -10534,11 +11877,11 @@ snapshots: dependencies: any-promise: 1.3.0 - three-mesh-bvh@0.7.8(three@0.171.0): + three-mesh-bvh@0.7.8(three@0.172.0): dependencies: - three: 0.171.0 + three: 0.172.0 - three-stdlib@2.34.0(three@0.171.0): + three-stdlib@2.34.0(three@0.172.0): dependencies: '@types/draco3d': 1.4.10 '@types/offscreencanvas': 2019.7.3 @@ -10546,9 +11889,9 @@ snapshots: draco3d: 1.5.7 fflate: 0.6.10 potpack: 1.0.2 - three: 0.171.0 + three: 0.172.0 - three@0.171.0: {} + three@0.172.0: {} through@2.3.8: {} @@ -10566,6 +11909,12 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@6.1.72: {} + + tldts@6.1.72: + dependencies: + tldts-core: 6.1.72 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -10585,21 +11934,29 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tough-cookie@5.1.0: + dependencies: + tldts: 6.1.72 + tr46@0.0.3: {} + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} - troika-three-text@0.52.2(three@0.171.0): + troika-three-text@0.52.2(three@0.172.0): dependencies: bidi-js: 1.0.3 - three: 0.171.0 - troika-three-utils: 0.52.0(three@0.171.0) + three: 0.172.0 + troika-three-utils: 0.52.0(three@0.172.0) troika-worker-utils: 0.52.0 webgl-sdf-generator: 1.1.1 - troika-three-utils@0.52.0(three@0.171.0): + troika-three-utils@0.52.0(three@0.172.0): dependencies: - three: 0.171.0 + three: 0.172.0 troika-worker-utils@0.52.0: {} @@ -10868,13 +12225,13 @@ snapshots: vary@1.1.2: {} - vite-node@2.1.5(@types/node@22.9.0)(terser@5.36.0): + vite-node@2.1.5(@types/node@22.9.0)(terser@5.37.0): dependencies: cac: 6.7.14 debug: 4.3.7 es-module-lexer: 1.5.4 pathe: 1.1.2 - vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.9.0)(terser@5.37.0) transitivePeerDependencies: - '@types/node' - less @@ -10886,18 +12243,32 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@5.1.3(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)): + vite-plugin-compression2@1.3.3(rollup@4.26.0)(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0)): + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.26.0) + tar-mini: 0.2.0 + vite: 5.4.11(@types/node@22.9.0)(terser@5.37.0) + transitivePeerDependencies: + - rollup + + vite-plugin-image-optimizer@1.1.8(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0)): + dependencies: + ansi-colors: 4.1.3 + pathe: 1.1.2 + vite: 5.4.11(@types/node@22.9.0)(terser@5.37.0) + + vite-tsconfig-paths@5.1.3(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0)): dependencies: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.4(typescript@5.6.3) optionalDependencies: - vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.9.0)(terser@5.37.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.11(@types/node@22.9.0)(terser@5.36.0): + vite@5.4.11(@types/node@22.9.0)(terser@5.37.0): dependencies: esbuild: 0.21.5 postcss: 8.4.49 @@ -10905,12 +12276,12 @@ snapshots: optionalDependencies: '@types/node': 22.9.0 fsevents: 2.3.3 - terser: 5.36.0 + terser: 5.37.0 - vitest@2.1.5(@types/node@22.9.0)(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(terser@5.36.0): + vitest@2.1.5(@types/node@22.9.0)(jsdom@26.0.0)(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(terser@5.37.0): dependencies: '@vitest/expect': 2.1.5 - '@vitest/mocker': 2.1.5(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)) + '@vitest/mocker': 2.1.5(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(vite@5.4.11(@types/node@22.9.0)(terser@5.37.0)) '@vitest/pretty-format': 2.1.5 '@vitest/runner': 2.1.5 '@vitest/snapshot': 2.1.5 @@ -10926,11 +12297,12 @@ snapshots: tinyexec: 0.3.1 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) - vite-node: 2.1.5(@types/node@22.9.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.9.0)(terser@5.37.0) + vite-node: 2.1.5(@types/node@22.9.0)(terser@5.37.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.9.0 + jsdom: 26.0.0 transitivePeerDependencies: - less - lightningcss @@ -10942,6 +12314,10 @@ snapshots: - supports-color - terser + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -10961,6 +12337,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + webpack-node-externals@3.0.0: {} webpack-sources@3.2.3: {} @@ -10997,6 +12375,17 @@ snapshots: - esbuild - uglify-js + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.1.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -11062,6 +12451,12 @@ snapshots: ws@8.17.1: {} + ws@8.18.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xmlhttprequest-ssl@2.1.2: {} xtend@4.0.2: {}