diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b1ce9c02..4e673a7b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,10 +30,18 @@ jobs: git config user.name farmsystem-account git config user.email ${{ secrets.EMAIL }} + # 게임 애들꺼 push 제외 + - name: Exclude Unity WebGL build (deploy-only) + run: | + # Remove from index if present, but do not fail if missing + git rm -r --cached --ignore-unmatch apps/farminglog/public/WebGLBuild || true + # Create a temporary commit only in the workflow context + git commit -m "chore(ci): exclude apps/farminglog/public/WebGLBuild from deploy sync" || echo "No changes to commit" + - name: Push changes to forked-repo run: | git push -f forked-repo ${{ env.TARGET_BRANCH }} - name: Clean up run: | - git remote remove forked-repo + git remote remove forked-repo \ No newline at end of file diff --git a/.gitignore b/.gitignore index c4bff3db..e6ad004b 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ dist-ssr *.sln *.sw? .vercel + diff --git a/apps/farminglog/src/components/Header/Header.tsx b/apps/farminglog/src/components/Header/Header.tsx index 3aec2c91..00b40046 100644 --- a/apps/farminglog/src/components/Header/Header.tsx +++ b/apps/farminglog/src/components/Header/Header.tsx @@ -7,6 +7,7 @@ import mainIcon from "@/assets/logos/logo.basic.png"; // 메인 아이콘 import crownIcon from "@/assets/Icons/tabler_crown.png"; // 랭킹 아이콘 import pencilIcon from "@/assets/Icons/edit-3.png"; // 파밍 아이콘 import thumbsUpIcon from "@/assets/Icons/goodgood.png"; // 응원 아이콘 +import gameIcon from "@/assets/Icons/Seed.png"; // 게임 아이콘 (씨앗 아이콘 사용) import useMediaQueries from "@/hooks/useMediaQueries"; import Popup from "@/components/Popup/popup"; @@ -17,6 +18,7 @@ const navItems = [ { label: "홈", path: "/home" }, { label: "응원", path: "/cheer" }, { label: "파밍로그", path: "/farminglog/view" }, + { label: "게임", path: "/game" }, { label: "랭킹", path: "/rankingDetail" }, ]; @@ -149,6 +151,16 @@ export default function Header() { } as React.CSSProperties} onClick={() => handleNavigation("/farminglog/view")} /> + handleNavigation("/game")} + /> )} setMenuOpen((prev) => !prev)}> diff --git a/apps/farminglog/src/components/UnityWebGL/UnityWebGL.styled.ts b/apps/farminglog/src/components/UnityWebGL/UnityWebGL.styled.ts new file mode 100644 index 00000000..ec5ac8d0 --- /dev/null +++ b/apps/farminglog/src/components/UnityWebGL/UnityWebGL.styled.ts @@ -0,0 +1,32 @@ +import styled from 'styled-components'; + +export const UnityContainer = styled.div` + position: relative; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + margin: 20px 0; +`; + +export const UnityWrapper = styled.iframe` + max-width: 100%; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + border-radius: 12px; + + &:hover { + box-shadow: 0 6px 25px rgba(0, 0, 0, 0.15); + } +`; + +// 반응형 디자인을 위한 미디어 쿼리 +export const ResponsiveUnityWrapper = styled(UnityWrapper)` + @media (max-width: 768px) { + width: 100% !important; + height: 400px !important; + } + + @media (max-width: 480px) { + height: 300px !important; + } +`; \ No newline at end of file diff --git a/apps/farminglog/src/components/UnityWebGL/UnityWebGL.tsx b/apps/farminglog/src/components/UnityWebGL/UnityWebGL.tsx new file mode 100644 index 00000000..dc2ae460 --- /dev/null +++ b/apps/farminglog/src/components/UnityWebGL/UnityWebGL.tsx @@ -0,0 +1,134 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { UnityContainer, UnityWrapper } from './UnityWebGL.styled'; + +interface UnityWebGLProps { + width?: string | number; + height?: string | number; + className?: string; +} + +const UnityWebGL: React.FC = ({ + width = '100%', + height = '1000px', + className +}) => { + const iframeRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + + useEffect(() => { + const iframe = iframeRef.current; + if (!iframe) return; + + // 타임아웃 설정 (30초) + const timeoutId = setTimeout(() => { + if (isLoading) { + setIsLoading(false); + setHasError(true); + } + }, 30000); + + const handleLoad = () => { + clearTimeout(timeoutId); + setIsLoading(false); + setHasError(false); + }; + + const handleError = () => { + clearTimeout(timeoutId); + setIsLoading(false); + setHasError(true); + }; + + iframe.addEventListener('load', handleLoad); + iframe.addEventListener('error', handleError); + + return () => { + clearTimeout(timeoutId); + iframe.removeEventListener('load', handleLoad); + iframe.removeEventListener('error', handleError); + }; + }, [isLoading]); + + if (hasError) { + return ( + +
+

Unity WebGL 게임을 로드할 수 없습니다

+

가능한 해결 방법:

+
    +
  • 브라우저를 새로고침해보세요
  • +
  • WebGL을 지원하는 브라우저를 사용하세요 (Chrome, Firefox, Safari)
  • +
  • 개발자 도구 콘솔에서 에러 메시지를 확인하세요
  • +
+ +
+
+ ); + } + + return ( + + {isLoading && ( +
+

Unity 게임 로딩 중...

+
+ )} + + +
+ ); +}; + +export default UnityWebGL; \ No newline at end of file diff --git a/apps/farminglog/src/components/UnityWebGL/index.ts b/apps/farminglog/src/components/UnityWebGL/index.ts new file mode 100644 index 00000000..4c638bfb --- /dev/null +++ b/apps/farminglog/src/components/UnityWebGL/index.ts @@ -0,0 +1,2 @@ +export { default as UnityWebGL } from './UnityWebGL'; +export * from './UnityWebGL.styled'; \ No newline at end of file diff --git a/apps/farminglog/src/hooks/useNotificationSSE.ts b/apps/farminglog/src/hooks/useNotificationSSE.ts index 0e8858ae..5ce060f0 100644 --- a/apps/farminglog/src/hooks/useNotificationSSE.ts +++ b/apps/farminglog/src/hooks/useNotificationSSE.ts @@ -35,6 +35,7 @@ export const useNotificationSSE = () => { .getReader(); let buffer = ''; + // eslint-disable-next-line no-constant-condition while (true) { const { value, done } = await reader.read(); if (done) break; @@ -54,8 +55,8 @@ export const useNotificationSSE = () => { buffer = lines[lines.length - 1]; } - } catch (err: any) { - if (err.name === 'AbortError') { + } catch (err: unknown) { + if (err instanceof Error && err.name === 'AbortError') { console.log('SSE 연결 중단됨'); } else { console.error('SSE 연결 오류:', err); diff --git a/apps/farminglog/src/pages/UnityGame/index.styled.ts b/apps/farminglog/src/pages/UnityGame/index.styled.ts new file mode 100644 index 00000000..c12a0d3c --- /dev/null +++ b/apps/farminglog/src/pages/UnityGame/index.styled.ts @@ -0,0 +1,32 @@ +import styled from 'styled-components'; + +export const GameContainer = styled.div` + max-width: 1200px; + margin: 0 auto; + padding: 40px 20px; + min-height: 1000px; +`; + +export const GameTitle = styled.h1` + font-size: 2.5rem; + font-weight: bold; + text-align: center; + margin-bottom: 20px; + color: #333; + + @media (max-width: 768px) { + font-size: 2rem; + } +`; + +export const GameDescription = styled.p` + font-size: 1.1rem; + text-align: center; + color: #666; + margin-bottom: 30px; + line-height: 1.6; + + @media (max-width: 768px) { + font-size: 1rem; + } +`; diff --git a/apps/farminglog/src/pages/UnityGame/index.tsx b/apps/farminglog/src/pages/UnityGame/index.tsx new file mode 100644 index 00000000..86d0f27b --- /dev/null +++ b/apps/farminglog/src/pages/UnityGame/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { UnityWebGL } from '../../components/UnityWebGL'; +import { GameContainer, GameTitle, GameDescription } from './index.styled'; + +const UnityGame: React.FC = () => { + return ( + + Unity WebGL 게임 + + 아래에서 Unity로 제작된 WebGL 게임을 플레이할 수 있습니다. + 게임이 로드되지 않는 경우 브라우저를 새로고침해주세요. + + + + + ); +}; + +export default UnityGame; diff --git a/apps/farminglog/src/pages/auth/SocialRedirect.tsx b/apps/farminglog/src/pages/auth/SocialRedirect.tsx index d58843ff..d7d418d4 100644 --- a/apps/farminglog/src/pages/auth/SocialRedirect.tsx +++ b/apps/farminglog/src/pages/auth/SocialRedirect.tsx @@ -30,8 +30,8 @@ export default function SocialRedirect() { try { await socialLogin({ code, socialType: provider }); navigate('/home'); - } catch (error: any) { - const status = error?.status; + } catch (error: unknown) { + const status = (error as { status?: number })?.status; if (status === 404) { setErrorTitle( diff --git a/apps/farminglog/src/pages/auth/components/StepInputStudentId.tsx b/apps/farminglog/src/pages/auth/components/StepInputStudentId.tsx index 00ec376e..217c90e7 100644 --- a/apps/farminglog/src/pages/auth/components/StepInputStudentId.tsx +++ b/apps/farminglog/src/pages/auth/components/StepInputStudentId.tsx @@ -36,8 +36,8 @@ const handleNext = () => { setErrorMessage('회원 인증에 실패했습니다.'); } }, - onError: (err: any) => { - if (err.status === 401 || err.status === 404) { + onError: (err: unknown) => { + if ((err as { status?: number })?.status === 401 || (err as { status?: number })?.status === 404) { setStep('not-member'); } else { setErrorMessage('서버 오류입니다. 운영진에게 문의해주세요.'); diff --git a/apps/farminglog/src/pages/cheer/write/cheer.tsx b/apps/farminglog/src/pages/cheer/write/cheer.tsx index 7da805c6..2c70a73c 100644 --- a/apps/farminglog/src/pages/cheer/write/cheer.tsx +++ b/apps/farminglog/src/pages/cheer/write/cheer.tsx @@ -109,7 +109,7 @@ export default function CheerMessageEditor({ searchedUser }: CheerMessageEditorP await queryClient.invalidateQueries({ queryKey: ['cheerList'] }); await queryClient.invalidateQueries({ queryKey: ['user', 'today-seed'] }); - const updatedSeed = queryClient.getQueryData(['user', 'today-seed']); + const updatedSeed = queryClient.getQueryData<{ isCheer?: boolean }>(['user', 'today-seed']); const isFirstCheer = !todaySeed?.isCheer && updatedSeed?.isCheer; setPopupMessage({ diff --git a/apps/farminglog/src/pages/game/index.styled.ts b/apps/farminglog/src/pages/game/index.styled.ts new file mode 100644 index 00000000..c2800578 --- /dev/null +++ b/apps/farminglog/src/pages/game/index.styled.ts @@ -0,0 +1,124 @@ +import styled from 'styled-components'; + +export const GameContainer = styled.div` + max-width: 1200px; + margin: 0 auto; + padding: 20px; + min-height: calc(100vh - 70px); + + @media (max-width: 768px) { + padding: 15px; + min-height: calc(100vh - 55px); + } +`; + +export const GameTitle = styled.h1` + font-size: 2.5rem; + font-weight: bold; + text-align: center; + margin-bottom: 20px; + color: #2d5016; + text-shadow: 0 2px 4px rgba(0,0,0,0.1); + + @media (max-width: 768px) { + font-size: 2rem; + } +`; + +export const GameDescription = styled.p` + font-size: 1.1rem; + text-align: center; + color: #495057; + margin-bottom: 30px; + line-height: 1.6; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + padding: 20px; + border-radius: 12px; + border: 1px solid #dee2e6; + + @media (max-width: 768px) { + font-size: 1rem; + padding: 15px; + } +`; + +export const ControlsInfo = styled.div` + margin-top: 30px; + background: #ffffff; + border-radius: 12px; + padding: 25px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + border: 1px solid #e9ecef; + + h3 { + margin: 0 0 20px 0; + color: #2d5016; + font-size: 1.3rem; + text-align: center; + } + + .controls-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + } + } + + .control-item { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; + transition: all 0.2s ease; + + &:hover { + background: #e9ecef; + transform: translateY(-2px); + } + + strong { + color: #2d5016; + font-size: 1.1rem; + margin-bottom: 5px; + } + + span { + color: #6c757d; + font-size: 0.9rem; + text-align: center; + } + } +`; + +export const StartContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 1000px; /* UnityWebGL 컴포넌트와 동일한 높이를 주어 레이아웃을 유지합니다. */ + background-color: #f0f2f5; + border-radius: 12px; +`; + +export const StartButton = styled.button` + padding: 18px 35px; + font-size: 20px; + font-weight: bold; + color: white; + background-color: #2d5016; /* 기존 버튼과 통일감 있는 색상 */ + border: none; + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease-in-out; + + &:hover { + transform: scale(1.05); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + } +`; \ No newline at end of file diff --git a/apps/farminglog/src/pages/game/index.tsx b/apps/farminglog/src/pages/game/index.tsx new file mode 100644 index 00000000..92d41b93 --- /dev/null +++ b/apps/farminglog/src/pages/game/index.tsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; // useState를 import 합니다. +import { UnityWebGL } from '../../components/UnityWebGL'; +import { GameContainer, GameTitle, StartButton, StartContainer } from './index.styled.ts'; + +const Game: React.FC = () => { + const [isGameStarted, setIsGameStarted] = useState(false); + + const handleStartGame = () => { + setIsGameStarted(true); + }; + + return ( + + 🌱 Grow My Farm + + {/** isGameStarted 값에 따라 조건부로 렌더링. */} + {isGameStarted ? ( + + ) : ( + + + 게임 시작하기 + + + )} + + ); +}; + +export default Game; \ No newline at end of file diff --git a/apps/farminglog/src/pages/home/Harvest/harvest.styled.ts b/apps/farminglog/src/pages/home/Harvest/harvest.styled.ts index b07a81a8..cd0b3fbb 100644 --- a/apps/farminglog/src/pages/home/Harvest/harvest.styled.ts +++ b/apps/farminglog/src/pages/home/Harvest/harvest.styled.ts @@ -245,7 +245,9 @@ export const InfoButton = styled.div<{ }>` `; -export const TextContainer = styled.div<{ - -}>` +export const TextContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; `; \ No newline at end of file diff --git a/apps/farminglog/src/router/index.tsx b/apps/farminglog/src/router/index.tsx index c4e0ac02..a19bfdd5 100644 --- a/apps/farminglog/src/router/index.tsx +++ b/apps/farminglog/src/router/index.tsx @@ -13,6 +13,7 @@ import Create from "@/pages/farminglog/create"; import Mypage from "@/pages/myPage"; import Ranking from "@/pages/home/Ranking/ranking"; import RankingDetail from "@/pages/ranking"; +import Game from "@/pages/game"; import Error from "@/pages/error"; export const router = createBrowserRouter([ @@ -69,6 +70,11 @@ export const router = createBrowserRouter([ loader: protectedLoader, element: , }, + { + path: "/game", + loader: protectedLoader, + element: , + }, ], }, { diff --git a/apps/farminglog/src/stores/harvestStore.ts b/apps/farminglog/src/stores/harvestStore.ts index 6756a447..4eaa376c 100644 --- a/apps/farminglog/src/stores/harvestStore.ts +++ b/apps/farminglog/src/stores/harvestStore.ts @@ -28,9 +28,10 @@ const useButtonStore = create()( name: "button-active-states", // localStorage 키로 사용됨 version: 1, // migrate 함수에서 마지막 업데이트 시간이 오늘과 다르면 기본값으로 초기화 - migrate: (persistedState: any) => { + migrate: (persistedState: unknown) => { const now = new Date(); - const lastUpdate = new Date(persistedState.lastUpdate); + const state = persistedState as { lastUpdate?: string }; + const lastUpdate = new Date(state.lastUpdate || ''); if ( now.getFullYear() !== lastUpdate.getFullYear() || now.getMonth() !== lastUpdate.getMonth() || diff --git a/apps/farminglog/vercel.json b/apps/farminglog/vercel.json index 5cf0dcd4..de4bffb3 100644 --- a/apps/farminglog/vercel.json +++ b/apps/farminglog/vercel.json @@ -1,4 +1,41 @@ { + "headers": [ + { + "source": "/(.*)\\.js\\.br", + "headers": [ + { "key": "Content-Encoding", "value": "br" }, + { "key": "Content-Type", "value": "application/javascript; charset=utf-8" } + ] + }, + { + "source": "/(.*)\\.wasm\\.br", + "headers": [ + { "key": "Content-Encoding", "value": "br" }, + { "key": "Content-Type", "value": "application/wasm" } + ] + }, + { + "source": "/(.*)\\.data\\.br", + "headers": [ + { "key": "Content-Encoding", "value": "br" }, + { "key": "Content-Type", "value": "application/octet-stream" } + ] + }, + { + "source": "/(.*)\\.js\\.gz", + "headers": [ + { "key": "Content-Encoding", "value": "gzip" }, + { "key": "Content-Type", "value": "application/javascript; charset=utf-8" } + ] + }, + { + "source": "/(.*)\\.wasm\\.gz", + "headers": [ + { "key": "Content-Encoding", "value": "gzip" }, + { "key": "Content-Type", "value": "application/wasm" } + ] + } + ], "rewrites": [ { "source": "/auth/callback", "destination": "/index.html" }, { "source": "/(.*)", "destination": "/index.html" } diff --git a/apps/website/package.json b/apps/website/package.json index 335f4a88..b7af90ac 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -7,15 +7,19 @@ "dev": "vite --host", "type-check": "tsc --noEmit", "build": "tsc -b && vite build", + "build:api": "tsc -b", + "build:client": "vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "@repo/api": "workspace:*", + "@sparticuz/chromium-min": "^138.0.1", "@types/react-slick": "^0.23.13", "@types/styled-components": "^5.1.34", "axios": "^1.7.9", "framer-motion": "^12.4.3", + "puppeteer-core": "^24.14.0", "react-icons": "^5.4.0", "react-intersection-observer": "^9.15.1", "react-responsive": "^10.0.0", @@ -32,7 +36,8 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^15.14.0", - "typescript": "~5.6.2", + "ts-node": "^10.9.2", + "typescript": "~5.6.3", "typescript-eslint": "^8.18.2", "vite": "^6.0.5" } diff --git a/apps/website/src/assets/Icons/deploy_Icon.png b/apps/website/src/assets/Icons/deploy_Icon.png new file mode 100644 index 00000000..e06c0389 Binary files /dev/null and b/apps/website/src/assets/Icons/deploy_Icon.png differ diff --git a/apps/website/src/assets/Images/Blog_Project/blank_img_project.png b/apps/website/src/assets/Images/Blog_Project/blank_img_project.png new file mode 100644 index 00000000..dae0d148 Binary files /dev/null and b/apps/website/src/assets/Images/Blog_Project/blank_img_project.png differ diff --git a/apps/website/src/components/Header/Header.tsx b/apps/website/src/components/Header/Header.tsx index b9fdb6b8..063a2e81 100644 --- a/apps/website/src/components/Header/Header.tsx +++ b/apps/website/src/components/Header/Header.tsx @@ -113,8 +113,8 @@ export default function Header() { handleNavItemClick('/blog')} - isActive={location.pathname === '/blog'} + onClick={() => handleNavItemClick('/project')} + isActive={location.pathname === '/project'} > 블로그 / 프로젝트 diff --git a/apps/website/src/hooks/useLinkPreview.ts b/apps/website/src/hooks/useLinkPreview.ts index 14d41f3a..548570c8 100644 --- a/apps/website/src/hooks/useLinkPreview.ts +++ b/apps/website/src/hooks/useLinkPreview.ts @@ -1,12 +1,12 @@ -import { useState, useEffect, useRef } from 'react'; -import { handleApiError } from '@/utils/handleApiError'; +import { useState, useEffect, useRef } from "react"; +import { handleApiError } from "@/utils/handleApiError"; export interface APIResponse { title: string; description: string; image: string; - siteName: string; - hostname: string; + siteName?: string; + hostname?: string; } /** @@ -24,7 +24,7 @@ export const isValidResponse = (res: APIResponse | null): boolean => { }; // 프록시 서버 URL (CORS 헤더가 추가되어야 합니다) -// corsproxy.io 는 요청 URL을 쿼리스트링으로 전달합니다. +// corsproxy.io 는 요청 URL을 쿼리스트링으로 전달. // const proxyUrl = 'https://corsproxy.io/?key=****&url='; const proxyUrl = 'https://corsproxy.io/?url='; // localhost용 프록시 diff --git a/apps/website/src/layouts/DetailLayout/ProjectDetailLayout.styled.ts b/apps/website/src/layouts/DetailLayout/ProjectDetailLayout.styled.ts index c78eaa90..f3eb08ff 100644 --- a/apps/website/src/layouts/DetailLayout/ProjectDetailLayout.styled.ts +++ b/apps/website/src/layouts/DetailLayout/ProjectDetailLayout.styled.ts @@ -151,7 +151,7 @@ export const Introduction = styled.p` text-overflow: ellipsis; white-space: nowrap; - width: 50%; + width: 66%; max-width: 800px; `; @@ -163,16 +163,73 @@ export const ImageContainer = styled.div` `; export const Thumbnail = styled.img` - width: 827.92px; - // height: 533px; + width: 100%; + max-width: 827.92px; flex-shrink: 0; aspect-ratio: 827.92/533.00; - border-top: 3px solid var(--FarmSystem_DarkGrey, #999); + border-top: 1px solid var(--FarmSystem_DarkGrey, #999); border-bottom: 1px solid var(--FarmSystem_DarkGrey, #999); background: url() lightgray 50% / cover no-repeat; `; +//프로젝트 디테일 시 이미지 없어서 그냥 맞추어서 작업 +export const PlaceholderImage = styled.div` + width: 100%; + max-width: 827.92px; + flex-shrink: 0; + aspect-ratio: 827.92/533.00; + + display: flex; + align-items: center; + justify-content: center; + + background-color: var(--FarmSystem_LightGrey, #E5E5E5); + border-top: 3px solid var(--FarmSystem_DarkGrey, #999); + border-bottom: 1px solid var(--FarmSystem_DarkGrey, #999); + + color: var(--FarmSystem_DarkGrey, #999); + font-size: ${({ $isMobile }) => ($isMobile ? "16px": "20px")}; + font-weight: 500; +`; + +// 예쁜 기본 썸네일 +export const DefaultThumbnail = styled.div` + width: 100%; + max-width: 827.92px; + flex-shrink: 0; + aspect-ratio: 827.92/533.00; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-top: 3px solid var(--FarmSystem_DarkGrey, #999); + border-bottom: 1px solid var(--FarmSystem_DarkGrey, #999); + + transition: all 0.3s ease; + + &:hover { + background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%); + transform: translateY(-2px); + } +`; + +export const DefaultThumbnailIcon = styled.div` + font-size: 48px; + opacity: 0.7; +`; + +export const DefaultThumbnailText = styled.div` + font-size: ${({ $isMobile }) => ($isMobile ? "16px": "20px")}; + font-weight: 500; + color: var(--FarmSystem_DarkGrey, #6c757d); + text-align: center; +`; + export const ContentBox = styled.p` width: 100%; max-width: 800px; @@ -236,4 +293,4 @@ export const ParticipantContainer = styled.div` justify-content: flex-end; width: 100%; min-width: 70%; -`; \ No newline at end of file +`; diff --git a/apps/website/src/layouts/DetailLayout/ProjectDetailLayout.tsx b/apps/website/src/layouts/DetailLayout/ProjectDetailLayout.tsx index 63e4ce15..51dc20a5 100644 --- a/apps/website/src/layouts/DetailLayout/ProjectDetailLayout.tsx +++ b/apps/website/src/layouts/DetailLayout/ProjectDetailLayout.tsx @@ -2,8 +2,9 @@ import * as S from "./ProjectDetailLayout.styled"; import GoBackArrow from "@/assets/LeftArrow.png"; import useMediaQueries from "@/hooks/useMediaQueries"; import GithubIcon from "@/assets/githubLogo.png"; -import DeploymentIcon from "@/assets/black_link.png"; +import DeploymentIcon from "@/assets/Icons/deploy_Icon.png"; import ResourceIcon from "@/assets/pink_link.png"; +import BlankThumbnailImg from "@/assets/Images/Blog_Project/blank_img_project.png"; interface ProjectDetailLayoutProps { title?: string; @@ -23,7 +24,7 @@ export default function ProjectDetailLayout({ content = "(임시) 내용", introduction = "(임시) 소개", tag = "(임시) 태그", - thumbnailUrl = "", + thumbnailUrl = BlankThumbnailImg, githubLink = "(임시) 링크", deploymentLink = "(임시) 링크", resourceLink = "(임시) 링크", @@ -92,7 +93,7 @@ export default function ProjectDetailLayout({ diff --git a/apps/website/src/pages/Blog/Blog/BlogItem.styles.tsx b/apps/website/src/pages/Blog/Blog/BlogItem.styles.tsx index 8830c818..e04f23b8 100644 --- a/apps/website/src/pages/Blog/Blog/BlogItem.styles.tsx +++ b/apps/website/src/pages/Blog/Blog/BlogItem.styles.tsx @@ -3,8 +3,8 @@ import styled from 'styled-components'; // 스타일 컴포넌트 (블로그랑 같지만, 다를 수 있으니께) // 요긴 대충 짜서 해결 export const Card = styled.a<{$isMobile: boolean; $isTablet: boolean;}>` - width: ${(props) => (props.$isMobile ? '130px' : props.$isTablet ? '240px' : '300px')}; - height: ${(props) => (props.$isMobile ? '205px' : props.$isTablet ? '260px' : '335px')}; + width: ${(props) => (props.$isMobile ? '130px' : props.$isTablet ? '240px' : '250px')}; + height: ${(props) => (props.$isMobile ? '205px' : props.$isTablet ? '260px' : '279px')}; border-radius: ${(props) => (props.$isMobile ? '10px' : '8px')}; overflow: hidden; @@ -22,12 +22,19 @@ export const Image = styled.div<{$isMobile: boolean; $isTablet: boolean;}>` overflow: hidden; border-radius: 8px; - height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '200px')}; + height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '167px')}; background-color: var(--FarmSystem_LightGrey); + display: flex; + align-items: center; + justify-content: center; + img{ - height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '200px')}; + /* height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '200px')}; */ + width: 100%; + height: 100%; object-fit: cover; + object-position: center; } `; diff --git a/apps/website/src/pages/Blog/Blog/BlogItem.tsx b/apps/website/src/pages/Blog/Blog/BlogItem.tsx index b75ee464..9f2ea85f 100644 --- a/apps/website/src/pages/Blog/Blog/BlogItem.tsx +++ b/apps/website/src/pages/Blog/Blog/BlogItem.tsx @@ -1,8 +1,8 @@ import React from 'react'; import * as S from './BlogItem.styles'; -import { useLinkPreview } from '@/hooks/useLinkPreview'; import useMediaQueries from '@/hooks/useMediaQueries'; import BlankImg from '../../../assets/Images/Blog_Project/blank_img.svg'; +import { APIResponse } from '@/hooks/useLinkPreview'; export enum BlogCategory { SEMINAR = "SEMINAR", @@ -21,6 +21,8 @@ export interface BlogTag { export interface BlogItemProps { blogUrl: string; tags: BlogCategory[]; + metadata?: APIResponse; + loading?: boolean; } // 카테고리 enum을 텍스트로 매핑 @@ -45,19 +47,16 @@ const getCategoryName = (category: BlogCategory): string => { } }; -const BlogItem: React.FC = ({ blogUrl, tags }) => { - // blogUrl을 기반으로 메타데이터를 fetching - const { metadata, loading} = useLinkPreview(blogUrl); +const BlogItem: React.FC = ({ blogUrl, tags, metadata, loading }) => { const { isMobile, isTablet } = useMediaQueries(); - // 메타데이터가 없는 경우 대비 기본값 설정 const title = metadata?.title || '제목이 없습니다'; const description = metadata?.description || '설명이 없습니다'; const previewImage = - metadata?.image && metadata?.image !== 'null' && !metadata.image.startsWith('/') - ? metadata.image - : BlankImg; + metadata?.image && metadata?.image !== 'null' && !metadata.image.startsWith('/') + ? metadata.image + : BlankImg; return ( diff --git a/apps/website/src/pages/Blog/Blog/BlogList.styles.tsx b/apps/website/src/pages/Blog/Blog/BlogList.styles.tsx index a946c777..541d599a 100644 --- a/apps/website/src/pages/Blog/Blog/BlogList.styles.tsx +++ b/apps/website/src/pages/Blog/Blog/BlogList.styles.tsx @@ -129,16 +129,34 @@ export const ListContainer = styled.div<{$isTablet: boolean; $isBig: boolean; $i grid-template-columns: ${(props) => { if (props.$isMobile) return '1fr 1fr'; // 모바일: 2 컬럼 - return 'repeat(auto-fit, 300px)'; // 데스크탑: 자동 너비 조정 + return 'repeat(auto-fit, minmax(240px, 1fr))'; // 데스크탑: 유동 폭 컬럼 }}; - gap: ${(props) => (props.$isMobile ? '15px' : props.$isTablet ? '20px' : '20px')} - ${(props) => (props.$isMobile ? '10px' : props.$isTablet ? '10px' : props.$isBig ? '4vw' : '10vw')}; + /* 프로젝트 카드와 동일, gap을 clamp */ + gap: clamp(10px, 2vw, 24px) clamp(10px, 1vw, 16px); justify-items: ${(props) => (props.$isMobile || props.$isTablet ? 'start' : 'start')}; `; + +export const SkeletonListContainer = styled(ListContainer)``; + +export const SkeletonCard = styled.div<{$isMobile: boolean;}>` + width: 100%; + /* 반응형 높이: 최소 150px, 화면 커지면 증가, 최대 200px */ + height: clamp(150px, 14vw, 200px); + border-radius: ${(props) => (props.$isMobile ? '10px' : '16px')}; + background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%); + background-size: 400% 100%; + animation: skeleton-shimmer 1.2s ease-in-out infinite; + + @keyframes skeleton-shimmer { + 0% { background-position: 100% 50%; } + 100% { background-position: 0 50%; } + } +`; + /* 비어 있을 떄 출력하는 레이아웃 잡는 컨테이너 */ export const DescriptionContainer = styled.div` width: 100%; diff --git a/apps/website/src/pages/Blog/Blog/BlogList.tsx b/apps/website/src/pages/Blog/Blog/BlogList.tsx index 02afe037..446fc745 100644 --- a/apps/website/src/pages/Blog/Blog/BlogList.tsx +++ b/apps/website/src/pages/Blog/Blog/BlogList.tsx @@ -1,76 +1,33 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import * as S from './BlogList.styles'; import BlogItem, { BlogCategory } from './BlogItem'; import useMediaQueries from '@/hooks/useMediaQueries'; import { useBlogPage } from '@/hooks/useBlog'; +import { useLinkPreviewStore } from '@/stores/useLinkPreviewStore'; import nextArrow_left from '@/assets/Icons/pagenation_2.png'; import nextArrow_right from '@/assets/Icons/pagenation_2.png'; import jumpArrow_left from '@/assets/Icons/pagenation_1.png'; import jumpArrow_right from '@/assets/Icons/pagenation_1.png'; -/** 샘플용 더미 데이터 */ -/* -let blogData: BlogItemProps[]; -// 개발 모드에서만 보이게 수정했습니다. -if (import.meta.env.MODE === 'development' || window.location.hostname.startsWith('dev.')) { - blogData = [ - { - blogUrl: 'https://velog.io/', - tags: [{ category: BlogCategory.SEMINAR }], - }, - { - blogUrl: 'https://blog.naver.com/educds/222797324049', - tags: [{ category: BlogCategory.PROJECT }], - }, - { - blogUrl: 'https://blog.encrypted.gg/', - tags: [{ category: BlogCategory.STUDY }], - }, - { - blogUrl: 'https://www.github.com', - tags: [{ category: BlogCategory.HACKATHON }], - }, - { - blogUrl: 'https://ludeno-studying.tistory.com/82', - tags: [{ category: BlogCategory.REVIEW }], - }, - { - blogUrl: 'https://toss.im/', - tags: [{ category: BlogCategory.LECTURE }], - }, - ]; -} else { - blogData = []; -} -*/ - // 문자열을 BlogCategory로 변환하는 함수 const convertStringToBlogCategory = (categoryStr: string): BlogCategory => { switch (categoryStr) { case 'SEMINAR': - console.log('SEMINAR'); return BlogCategory.SEMINAR; case 'PROJECT': - console.log('PROJECT'); return BlogCategory.PROJECT; case 'STUDY': - console.log('STUDY'); return BlogCategory.STUDY; case 'HACKATHON': - console.log('HACKATHON'); return BlogCategory.HACKATHON; case 'REVIEW': - console.log('REVIEW'); return BlogCategory.REVIEW; case 'LECTURE': - console.log('LECTURE'); return BlogCategory.LECTURE; - case 'ETC': - console.log('ETC'); + case 'ETC': return BlogCategory.ETC; default: - console.log('기타'); return BlogCategory.ETC; } }; @@ -89,6 +46,24 @@ const BlogList: React.FC = () => { size: pageSize, }); + // zustand store + const getPreviewBatch = useLinkPreviewStore(state => state.getPreviewBatch); + const previewMap = useLinkPreviewStore(state => state.previewMap); + const loadingMap = useLinkPreviewStore(state => state.loadingMap); + + // 3개씩 배치로 LinkPreview 요청 + useEffect(() => { + if (!blogData?.content) return; + const urls = blogData.content.map(blog => blog.link); + const batchSize = 3; + const runBatches = async () => { + for (let i = 0; i < urls.length; i += batchSize) { + await getPreviewBatch(urls.slice(i, i + batchSize)); + } + }; + runBatches(); + }, [blogData, getPreviewBatch]); + // 페이지네이션 핸들러 const handlePageChange = (page: number) => { setCurrentPage(page); @@ -117,7 +92,7 @@ const BlogList: React.FC = () => { // 최대 7개의 페이지 번호만 표시 const maxVisiblePages = 3; let startPage = Math.max(0, current - Math.floor(maxVisiblePages / 2)); - let endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 1); + const endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 1); // 시작 페이지 조정 if (endPage - startPage < maxVisiblePages - 1) { @@ -132,7 +107,22 @@ const BlogList: React.FC = () => { }; if (loading) { - return
로딩 중...
; + return ( + + + + * 블로그 클릭 시 외부 링크로 연결돼요. + + + + + {Array.from({ length: 8 }).map((_, idx) => ( + + ))} + + + + ); } if (error) { @@ -148,7 +138,7 @@ const BlogList: React.FC = () => { {/* 블로그 카드 리스트 */} - {blogData && blogData.content.length > 0 ? ( + {blogData?.content && blogData.content.length > 0 ? ( <> @@ -160,63 +150,65 @@ const BlogList: React.FC = () => { ? blog.categories.map(str=> convertStringToBlogCategory(str)) : [BlogCategory.ETC] } + metadata={previewMap.get(blog.link) || undefined} + loading={loadingMap.get(blog.link) || false} /> ))} - {/* 페이지네이션 */} - {pageInfo && pageInfo.totalPages > 0 && ( - - - setCurrentPage(0)} - $disabled={!pageInfo.hasPreviousPage} - $isMobile={isMobile} - $isTablet={isTablet} - > - jumpArrow - - - nextArrow - - - {generatePageNumbers().map((pageNum) => ( - handlePageChange(pageNum)} - $active={pageNum === pageInfo.currentPage} - $isMobile={isMobile} - $isTablet={isTablet} - > - {pageNum + 1} - - ))} - - - nextArrow_right - - setCurrentPage(pageInfo.totalPages - 1)} - $disabled={!pageInfo.hasPreviousPage} + {/* 페이지네이션 */} + {pageInfo && pageInfo.totalPages > 0 && ( + + + setCurrentPage(0)} + $disabled={!pageInfo.hasPreviousPage} + $isMobile={isMobile} + $isTablet={isTablet} + > + jumpArrow + + + nextArrow + + + {generatePageNumbers().map((pageNum) => ( + handlePageChange(pageNum)} + $active={pageNum === pageInfo.currentPage} $isMobile={isMobile} $isTablet={isTablet} > - jumpArrow_right - - - - )} + {pageNum + 1} + + ))} + + + nextArrow_right + + setCurrentPage(pageInfo.totalPages - 1)} + $disabled={!pageInfo.hasNextPage} + $isMobile={isMobile} + $isTablet={isTablet} + > + jumpArrow_right + + + + )} ) : ( diff --git a/apps/website/src/pages/Blog/Project/ProjectDetail.styles.tsx b/apps/website/src/pages/Blog/Project/ProjectDetail.styles.tsx index 45f6dafd..884c2e8c 100644 --- a/apps/website/src/pages/Blog/Project/ProjectDetail.styles.tsx +++ b/apps/website/src/pages/Blog/Project/ProjectDetail.styles.tsx @@ -45,13 +45,64 @@ export const LoadingContainer = styled.div` color: var(--FarmSystem_Black); `; -export const ErrorContainer = styled.div` +export const ErrorContainer = styled.div` display: flex; + flex-direction: column; justify-content: center; align-items: center; min-height: 400px; - font-size: 18px; - color: var(--FarmSystem_Red); + padding: ${({ $isMobile }) => ($isMobile ? "20px": "40px")}; + text-align: center; + gap: 20px; +`; + +export const ErrorIcon = styled.div` + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, #ff6b6b, #ff8e8e); + display: flex; + align-items: center; + justify-content: center; + font-size: 40px; + color: white; + margin-bottom: 10px; +`; + +export const ErrorTitle = styled.h2` + font-size: ${({ $isMobile }) => ($isMobile ? "24px": "32px")}; + font-weight: 700; + color: var(--FarmSystem_Black); + margin: 0; +`; + +export const ErrorMessage = styled.p` + font-size: ${({ $isMobile }) => ($isMobile ? "16px": "18px")}; + color: var(--FarmSystem_DarkGrey); + margin: 0; + line-height: 1.5; +`; + +export const RetryButton = styled.button` + padding: ${({ $isMobile }) => ($isMobile ? "12px 24px": "16px 32px")}; + background: var(--FarmSystem_Green01); + color: white; + border: none; + border-radius: 8px; + font-size: ${({ $isMobile }) => ($isMobile ? "14px": "16px")}; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + margin-top: 10px; + + &:hover { + background: var(--FarmSystem_Green02); + transform: translateY(-2px); + } + + &:active { + transform: translateY(0); + } `; export const Section = styled.section` @@ -113,4 +164,48 @@ export const LinkButton = styled.a` &:hover { background-color: var(--FarmSystem_Green02); } +`; + +// 간단한 스켈레톤 컨테이너 +export const SkeletonContainer = styled.div` + padding: ${({ $isMobile }) => ($isMobile ? "100px 8px": "100px 20px")}; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + min-height: 100vh; + background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%); + background-size: 400% 100%; + animation: skeleton-shimmer 1.2s ease-in-out infinite; + color: var(--FarmSystem_Black); + font-size: 18px; + + @keyframes skeleton-shimmer { + 0% { background-position: 100% 50%; } + 100% { background-position: 0 50%; } + } +`; + +// 스켈레톤 레이아웃 카드 +export const SkeletonDetailCard = styled.div` + display: flex; + width: 100%; + max-width: 1000px; + padding: ${({ $isMobile }) => ($isMobile ? "20px": "50px")}; + flex-direction: column; + justify-content: flex-end; + align-items: center; + border-radius: 20px; + background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%); + background-size: 400% 100%; + animation: skeleton-shimmer 1.2s ease-in-out infinite; + box-shadow: 0px 0px 20px 5px var(--FarmSystem_LightGrey, #E5E5E5); + gap: ${({ $isMobile }) => ($isMobile ? "20px": "70px")}; + min-height: 600px; + + @keyframes skeleton-shimmer { + 0% { background-position: 100% 50%; } + 100% { background-position: 0 50%; } + } `; \ No newline at end of file diff --git a/apps/website/src/pages/Blog/Project/ProjectDetail.tsx b/apps/website/src/pages/Blog/Project/ProjectDetail.tsx index 7575e552..e8873a40 100644 --- a/apps/website/src/pages/Blog/Project/ProjectDetail.tsx +++ b/apps/website/src/pages/Blog/Project/ProjectDetail.tsx @@ -31,11 +31,6 @@ const ProjectDetail: React.FC = () => { Logger.error(err); } finally { setLoading(false); - return( - - 로딩에 실패했습니다. - - ) } }; @@ -45,15 +40,36 @@ const ProjectDetail: React.FC = () => { if (loading) { return ( - 로딩 중... + + 프로젝트 + + ); } - if (error || !project) { + if ((error || !project)) { return ( - 에러가 발생했습니다: {error?.message} + + 프로젝트 + + + + 프로젝트를 불러올 수 없습니다 + + + {error?.message || '프로젝트 정보를 찾을 수 없습니다.'} + + window.location.reload()} + > + 다시 시도 + + ); } diff --git a/apps/website/src/pages/Blog/Project/ProjectItem.style.tsx b/apps/website/src/pages/Blog/Project/ProjectItem.style.tsx index afde9efd..ba66af41 100644 --- a/apps/website/src/pages/Blog/Project/ProjectItem.style.tsx +++ b/apps/website/src/pages/Blog/Project/ProjectItem.style.tsx @@ -3,8 +3,8 @@ import styled from 'styled-components'; // 스타일 컴포넌트 (블로그랑 같지만, 다를 수 있으니께) // 요긴 대충 짜서 해결 export const Card = styled.div<{$isMobile: boolean; $isTablet: boolean;}>` - width: ${(props) => (props.$isMobile ? '130px' : props.$isTablet ? '240px' : '300px')}; - height: ${(props) => (props.$isMobile ? '205px' : props.$isTablet ? '260px' : '335px')}; + width: ${(props) => (props.$isMobile ? '130px' : props.$isTablet ? '240px' : '250px')}; + height: ${(props) => (props.$isMobile ? '205px' : props.$isTablet ? '260px' : '279px')}; border-radius: ${(props) => (props.$isMobile ? '10px' : '8px')}; overflow: hidden; @@ -21,13 +21,20 @@ export const Image = styled.div<{$isMobile: boolean; $isTablet: boolean;}>` overflow: hidden; border-radius: 8px; - height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '200px')}; + height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '167px')}; background-color: var(--FarmSystem_LightGrey); + + display: flex; + align-items: center; + justify-content: center; img{ - height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '200px')}; + /* height: ${(props) => (props.$isMobile ? '87px' : props.$isTablet ? '150px' : '200px')}; */ + width: 100%; + height: 100%; object-fit: cover; -} + object-position: center; + } `; export const Content = styled.div` @@ -38,7 +45,9 @@ export const Title = styled.h3<{$isMobile: boolean; $isTablet: boolean;}>` margin: 0px; font-size: ${(props) => (props.$isMobile ? '12px' : props.$isTablet ? '16px' : '24px')}; font-weight: 700; - + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; `; export const Description = styled.p<{$isMobile: boolean; $isTablet: boolean;}>` diff --git a/apps/website/src/pages/Blog/Project/ProjectItem.tsx b/apps/website/src/pages/Blog/Project/ProjectItem.tsx index b6a91d5b..33350145 100644 --- a/apps/website/src/pages/Blog/Project/ProjectItem.tsx +++ b/apps/website/src/pages/Blog/Project/ProjectItem.tsx @@ -2,6 +2,7 @@ import * as S from './ProjectItem.style'; import { Track } from '@/models/blog'; import { useNavigate } from 'react-router'; import useMediaQueries from '@/hooks/useMediaQueries'; +import BlankImg from '@/assets/Images/Blog_Project/blank_img.svg'; // 카테고리 enum을 텍스트 매핑 export const getProjectGeneration = (generation: number): string => { @@ -41,7 +42,7 @@ const ProjectItem: React.FC = ({ projectId, title, description return ( navigate(`/project/${projectId}`)}> - {title} + {title} {title} {description} diff --git a/apps/website/src/pages/Blog/Project/ProjectList.styles.tsx b/apps/website/src/pages/Blog/Project/ProjectList.styles.tsx index b987777c..8b493483 100644 --- a/apps/website/src/pages/Blog/Project/ProjectList.styles.tsx +++ b/apps/website/src/pages/Blog/Project/ProjectList.styles.tsx @@ -108,7 +108,6 @@ export const DropdownItem = styled.div<{$isMobile: boolean; $isTablet: boolean;} } `; -/** 프로젝트 리스트(카드)들을 감싸는 컨테이너 */ export const ListContainer = styled.div<{$isTablet: boolean; $isBig: boolean; $isMobile: boolean;}>` width: 100%; @@ -120,22 +119,40 @@ export const ListContainer = styled.div<{$isTablet: boolean; $isBig: boolean; $i grid-template-columns: ${(props) => { if (props.$isMobile) return '1fr 1fr'; // 모바일: 2 컬럼 - return 'repeat(auto-fit, 300px)'; // 데스크탑: 자동 너비 조정 + return 'repeat(auto-fit, minmax(240px, 1fr))'; // 데스크탑: 유동 폭 컬럼 }}; - gap: ${(props) => (props.$isMobile ? '15px' : props.$isTablet ? '20px' : '20px')} - ${(props) => (props.$isMobile ? '10px' : props.$isTablet ? '10px' : props.$isBig ? '4vw' : '10vw')}; + /* 블로그 카드와 동일, gap을 clamp로 반응형 설정? clamp 싱기싱기 */ + gap: clamp(10px, 2vw, 24px) clamp(10px, 1vw, 16px); justify-items: ${(props) => (props.$isMobile || props.$isTablet ? 'start' : 'start')}; `; +export const SkeletonListContainer = styled(ListContainer)``; + +export const SkeletonCard = styled.div<{$isMobile: boolean;}>` + width: 100%; + /* 반응형 높이: 최소 150px, 화면 커지면 증가, 최대 200px */ + height: clamp(150px, 14vw, 200px); + border-radius: ${(props) => (props.$isMobile ? '10px' : '16px')}; + background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%); + background-size: 400% 100%; + animation: skeleton-shimmer 1.2s ease-in-out infinite; + + @keyframes skeleton-shimmer { + 0% { background-position: 100% 50%; } + 100% { background-position: 0 50%; } + } +`; + /* 비어 있을 떄 출력하는 레이아웃 잡는 컨테이너 */ export const DescriptionContainer = styled.div` width: 100%; - margin: 20px; - display: block; + margin: 20px auto; + display: flex; + margin-bottom: 100px; `; /* 텍스트 컨테이너*/ diff --git a/apps/website/src/pages/Blog/Project/ProjectList.tsx b/apps/website/src/pages/Blog/Project/ProjectList.tsx index b42315f1..8d619507 100644 --- a/apps/website/src/pages/Blog/Project/ProjectList.tsx +++ b/apps/website/src/pages/Blog/Project/ProjectList.tsx @@ -97,7 +97,7 @@ const ProjectList: React.FC = () => { // 최대 7개의 페이지 번호만 표시 const maxVisiblePages = 3; let startPage = Math.max(0, current - Math.floor(maxVisiblePages / 2)); - let endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 1); + const endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 1); // 시작 페이지 조정 if (endPage - startPage < maxVisiblePages - 1) { @@ -113,7 +113,29 @@ const ProjectList: React.FC = () => { }; if (loading) { - return
로딩 중...
; + return ( + + + + + + 기수 + + + + + 트랙 + + + + + + {Array.from({ length: 8 }).map((_, idx) => ( + + ))} + + + ); } if (error) { diff --git a/apps/website/src/pages/News/NewsItem.styled.ts b/apps/website/src/pages/News/NewsItem.styled.ts index 73801396..75975096 100644 --- a/apps/website/src/pages/News/NewsItem.styled.ts +++ b/apps/website/src/pages/News/NewsItem.styled.ts @@ -2,10 +2,13 @@ import styled from 'styled-components'; interface MobileProps { $isMobile?: boolean; + $isTablet?: boolean; + $isDesktop?: boolean; } export const NewsItem = styled.button` display: flex; + flex-direction: ${({ $isMobile, $isTablet }) => ($isMobile ? 'column' : $isTablet ? 'row' : 'row')}; padding: ${({ $isMobile }) => ($isMobile ? '10px 15px' : '20px 30px')}; align-items: center; gap: ${({ $isMobile }) => ($isMobile ? '15px' : '30px')}; @@ -18,7 +21,7 @@ export const NewsItem = styled.button` `; export const Thumbnail = styled.img` - width: ${({ $isMobile }) => ($isMobile ? '30%' : '311px')}; + width: ${({ $isMobile }) => ($isMobile ? 'auto' : '311px')}; height: ${({ $isMobile }) => ($isMobile ? 'auto' : '200px')}; flex-shrink: 0; aspect-ratio: ${({ $isMobile }) => ($isMobile ? '16/9' : '311/200')}; @@ -75,6 +78,23 @@ export const Content = styled.p` text-overflow: ellipsis; `; +export const DateAndTagBox = styled.div` + display: flex; + align-items: ${({ $isMobile }) => ($isMobile ? 'flex-start' : 'center')}; + justify-content: ${({ $isMobile }) => ($isMobile ? 'flex-start' : 'space-between')}; + width: 100%; + flex-direction: ${({ $isMobile }) => ($isMobile ? 'column' : 'row')}; +`; + +export const Date = styled.p` + color: var(--FarmSystem_Black, #191919); + font-size: ${({ $isMobile }) => ($isMobile ? '14px' : '16px')}; + font-style: normal; + font-weight: 400; + line-height: 30px; /* 187.5% */ + letter-spacing: -0.24px; +`; + export const TagBox = styled.div` display: flex; align-items: center; @@ -85,7 +105,7 @@ export const TagBox = styled.div` export const Tag = styled.div` display: flex; height: ${({ $isMobile }) => ($isMobile ? '25px' : '30px')}; - padding: ${({ $isMobile }) => ($isMobile ? '3px 10px' : '5px 15px')}; + padding: ${({ $isMobile }) => ($isMobile ? '2px 6px' : '5px 15px')}; justify-content: center; align-items: center; gap: ${({ $isMobile }) => ($isMobile ? '5px' : '10px')}; @@ -95,7 +115,7 @@ export const Tag = styled.div` color: var(--FarmSystem_White, #FCFCFC); text-align: center; - font-size: ${({ $isMobile }) => ($isMobile ? '14px' : '16px')}; + font-size: ${({ $isMobile }) => ($isMobile ? '9px' : '16px')}; font-style: normal; font-weight: 400; line-height: ${({ $isMobile }) => ($isMobile ? '16px' : '20px')}; diff --git a/apps/website/src/pages/News/NewsItem.tsx b/apps/website/src/pages/News/NewsItem.tsx index 8d4b4a92..e82fb096 100644 --- a/apps/website/src/pages/News/NewsItem.tsx +++ b/apps/website/src/pages/News/NewsItem.tsx @@ -4,10 +4,11 @@ import PlaceHolder from '@/assets/Images/news/PlaceHolder.png'; import Logger from '@/utils/Logger'; import { useNavigate } from 'react-router'; import useMediaQueries from '@/hooks/useMediaQueries'; +import { formatKoreanDateTimeNoHour } from '@/utils/formatKoreanDateTime'; export default function NewsItem({ newsListData }: { newsListData?: newsListData }) { const navigate = useNavigate(); - const isMobile = useMediaQueries().isMobile; + const { isMobile, isTablet } = useMediaQueries(); if (!newsListData) { return null; @@ -27,17 +28,21 @@ export default function NewsItem({ newsListData }: { newsListData?: newsListData return ( navigate(`/news/${newsListData.newsId}`)} > {title} {truncatedContent} - - {tags.map((tag, index) => ( - {tag} - ))} - + + 게시일자: {formatKoreanDateTimeNoHour(newsListData.createdAt)} + + {tags.map((tag, index) => ( + {tag} + ))} + + ); diff --git a/apps/website/src/pages/News/index.styled.ts b/apps/website/src/pages/News/index.styled.ts index d7899624..d99adbd8 100644 --- a/apps/website/src/pages/News/index.styled.ts +++ b/apps/website/src/pages/News/index.styled.ts @@ -81,3 +81,145 @@ export const Line = styled.hr` max-width: 1100px; margin: 30px 0; `; + + +/** 페이지네이션 컨테이너 */ +export const PaginationContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin-top: 40px; + margin-bottom: 40px; +`; + +/** 페이지네이션 버튼 컨테이너 */ +export const PaginationButton = styled.div` + display: flex; + gap: 10px; + align-items: center; +`; + +/** 페이지네이션 버튼 텍스트 */ +export const PaginationButtonText = styled.span<{ + $active?: boolean; + $disabled?: boolean; + $isMobile?: boolean; + $isTablet?: boolean; +}>` + border-radius: 6px; + cursor: ${(props) => (props.$disabled ? 'not-allowed' : 'pointer')}; + font-size: 14px; + font-weight: 500; + transition: all 0.2s ease; + + /* 사이즈 조절 */ + img[alt="nextArrow"]{ + width: ${(props) => (props.$isMobile ? '6px' : props.$isTablet ? '12px' : '15px')}; + height: ${(props) => (props.$isMobile ? '12px' : props.$isTablet ? '24px' : '30px')}; + margin-right: 10px; + } + + img[alt="jumpArrow"]{ + width: ${(props) => (props.$isMobile ? '24px' : props.$isTablet ? '48px' : '60px')}; + height: ${(props) => (props.$isMobile ? '24px' : props.$isTablet ? '48px' : '60px')}; + } + + /* nextArrow 이미지 회전 */ + img[alt="nextArrow_right"] { + width: ${(props) => (props.$isMobile ? '6px' : props.$isTablet ? '12px' : '15px')}; + height: ${(props) => (props.$isMobile ? '12px' : props.$isTablet ? '24px' : '30px')}; + transform: rotate(180deg); + margin-left: 10px; + } + + img[alt="jumpArrow_right"] { + width: ${(props) => (props.$isMobile ? '24px' : props.$isTablet ? '48px' : '60px')}; + height: ${(props) => (props.$isMobile ? '24px' : props.$isTablet ? '48px' : '60px')}; + transform: rotate(180deg); + } + + &:hover { + ${(props) => !props.$disabled && ` + background-color: ${props.$active ? 'var(--FarmSystem_Green06)' : '#f0f0f0'}; + transform: translateY(-1px); + `} + } + + &:active { + ${(props) => !props.$disabled && ` + transform: translateY(0); + `} + } +`; + +export const PaginationPageButton = styled.span<{ + $active?: boolean; + $disabled?: boolean; + $isMobile?: boolean; + $isTablet?: boolean; +}>` + width: ${(props) => (props.$isMobile ? '20px' : props.$isTablet ? '26px' : '40px')}; + height: ${(props) => (props.$isMobile ? '20px' : props.$isTablet ? '26px' : '40px')}; + display: flex; + justify-content: center; + align-items: center; + border-radius: ${(props) => (props.$isMobile ? '10px' : props.$isTablet ? '13px' : '20px')}; + cursor: ${(props) => (props.$disabled ? 'not-allowed' : 'pointer')}; + + background-color: ${(props) => (props.$active ? 'var(--FarmSystem_Green06)' : 'var(--FarmSystem_DarkGrey)')}; + color: white; + font-size: ${(props) => (props.$isMobile ? '8px' : props.$isTablet ? '12px' : '14px')}; + font-weight: 500; + transition: all 0.2s ease; +`; + +// 스켈레톤 컴포넌트들 +export const SkeletonContainer = styled.div` + padding: 100px 20px; + display: flex; + flex-direction: column; + justify-content: start; + align-items: center; + width: 100%; + min-height: 100vh; +`; + +export const SkeletonTitle = styled.div<{$isMobile: boolean;}>` + width: ${({ $isMobile }) => ($isMobile ? '120px' : '200px')}; + height: ${({ $isMobile }) => ($isMobile ? '24px' : '40px')}; + background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%); + background-size: 400% 100%; + animation: skeleton-shimmer 1.2s ease-in-out infinite; + border-radius: ${({ $isMobile }) => ($isMobile ? '4px' : '8px')}; + margin-bottom: ${({ $isMobile }) => ($isMobile ? '40px' : '70px')}; + + @keyframes skeleton-shimmer { + 0% { background-position: 100% 50%; } + 100% { background-position: 0 50%; } + } +`; + +export const SkeletonNewsContainer = styled.div<{$isMobile: boolean;}>` + display: flex; + flex-direction: column; + gap: ${({ $isMobile }) => ($isMobile ? '15px' : '20px')}; + width: 100%; + max-width: 1000px; + margin-top: ${({ $isMobile }) => ($isMobile ? '40px' : '70px')}; +`; + +export const SkeletonNewsItem = styled.div<{$isMobile: boolean;}>` + width: 100%; + height: ${({ $isMobile }) => ($isMobile ? '80px' : '120px')}; + background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%); + background-size: 400% 100%; + animation: skeleton-shimmer 1.2s ease-in-out infinite; + border-radius: ${({ $isMobile }) => ($isMobile ? '6px' : '8px')}; + + @keyframes skeleton-shimmer { + 0% { background-position: 100% 50%; } + 100% { background-position: 0 50%; } + } +`; + + diff --git a/apps/website/src/pages/News/index.tsx b/apps/website/src/pages/News/index.tsx index 1e8eded7..280335e1 100644 --- a/apps/website/src/pages/News/index.tsx +++ b/apps/website/src/pages/News/index.tsx @@ -1,20 +1,70 @@ +import { useState, useEffect } from 'react'; import useMediaQueries from '@/hooks/useMediaQueries'; import { useNewsList } from '@/hooks/useNews'; // import Logger from '@/utils/Logger'; import * as S from './index.styled'; import NewsItem from './NewsItem'; +import jumpArrow_left from '@/assets/Icons/pagenation_1.png'; +import jumpArrow_right from '@/assets/Icons/pagenation_1.png'; +import nextArrow_left from '@/assets/Icons/pagenation_2.png'; +import nextArrow_right from '@/assets/Icons/pagenation_2.png'; + export default function News() { + const [currentPage, setCurrentPage] = useState(0); + const pageSize = 12; // 12개씩 페이지네이션 const { isMobile } = useMediaQueries(); const { data: newsData, loading: newsLoading, error: newsError } = useNewsList(); const newsDataSorted = newsData?.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - + const totalPages = Math.ceil(newsDataSorted?.length / pageSize); + + const currentNews = newsDataSorted?.slice( + currentPage * pageSize, + (currentPage + 1) * pageSize + ); + + const handlePreviousPage = () => { + if (currentPage > 0) setCurrentPage(currentPage - 1); + }; + + const handleNextPage = () => { + if (currentPage < totalPages - 1) setCurrentPage(currentPage + 1); + }; + + // 페이지 번호 배열 생성 + const generatePageNumbers = () => { + const pages: number[] = []; + const maxVisiblePages = 3; + let startPage = Math.max(0, currentPage - Math.floor(maxVisiblePages / 2)); + const endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 1); + + if (endPage - startPage < maxVisiblePages - 1) { + startPage = Math.max(0, endPage - maxVisiblePages + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + return pages; + }; + + // 페이지 전환시 스크롤을 맨위로 부드럽게 전환 + useEffect(() => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }, [currentPage]); + if (newsLoading) { return ( - - Loading... - + + 소식 + + {Array.from({ length: 6 }).map((_, idx) => ( + + ))} + + ); } @@ -27,23 +77,48 @@ export default function News() { ); - // Logger.error(newsError); - // return ( - // - // 뉴스를 불러오는 중 오류가 발생했습니다. - // - // ); } return ( 소식 {newsDataSorted && newsDataSorted.length > 0 ? ( - - {newsDataSorted.map((news, index) => ( - - ))} - + <> + + {currentNews?.map((news, index) => ( + + ))} + + {totalPages > 1 && ( + + + setCurrentPage(0)} $disabled={currentPage === 0}> + jumpArrow + + + nextArrow + + + {generatePageNumbers().map((pageNum) => ( + setCurrentPage(pageNum)} + $active={pageNum === currentPage} + > + {pageNum + 1} + + ))} + + = totalPages - 1}> + nextArrow_right + + setCurrentPage(totalPages - 1)} $disabled={currentPage >= totalPages - 1}> + jumpArrow_right + + + + )} + ) : ( 아직 등록된 소식이 없어요. diff --git a/apps/website/src/services/blog.ts b/apps/website/src/services/blog.ts index 066a52bc..70be83aa 100644 --- a/apps/website/src/services/blog.ts +++ b/apps/website/src/services/blog.ts @@ -50,6 +50,8 @@ export const getBlogPageList = async ( const url = `blogs/page?${queryString}`; - const response = await apiConfig.get(url); - return response.data; + + const response = await apiConfig.get<{ data: BlogPage }>(url); + console.log(response.data.data); + return response.data.data; }; diff --git a/apps/website/src/stores/useLinkPreviewStore.ts b/apps/website/src/stores/useLinkPreviewStore.ts new file mode 100644 index 00000000..9fd1982f --- /dev/null +++ b/apps/website/src/stores/useLinkPreviewStore.ts @@ -0,0 +1,125 @@ +import { create } from 'zustand'; +import { APIResponse, isValidResponse } from '@/hooks/useLinkPreview'; + +interface LinkPreviewState { + previewMap: Map; + loadingMap: Map; + getPreview: (url: string) => Promise; + getPreviewBatch: (urls: string[]) => Promise; +} + +// 프록시 서버 URL (useLinkPreview와 동일) +const proxyUrl = 'https://corsproxy.io/?url='; + +/** + * extractMetaContent + * 정규표현식을 사용하여 HTML에서 meta 태그의 content 값을 추출합니다. + */ +const extractMetaContent = (html: string, key: string): string => { + const regex = new RegExp( + `]*(?:property|name)=["']${key}["'][^>]*content=["']([^"']*)["'][^>]*>`, + 'i' + ); + const match = html.match(regex); + return match ? match[1] : ''; +}; + +/** + * cleanHTML + * HTML 문자열에서 주석을 제거합니다. + */ +const cleanHTML = (html: string): string => { + return html.replace(//g, ''); +}; + +/** + * parseHTML + * HTML 문자열을 파싱하여 OG 메타 태그 정보를 추출합니다. + */ +const parseHTML = (html: string, originalUrl: string): APIResponse => { + const clean = cleanHTML(html); + const parser = new DOMParser(); + const doc = parser.parseFromString(clean, 'text/html'); + + // OG 메타 태그 추출 + let title = + doc.querySelector('meta[property="og:title"], meta[name="og:title"]')?.getAttribute('content') || ''; + let description = + doc.querySelector('meta[property="og:description"], meta[name="og:description"]')?.getAttribute('content') || ''; + let image = + doc.querySelector('meta[property="og:image"], meta[name="og:image"]')?.getAttribute('content') || ''; + let siteName = + doc.querySelector('meta[property="og:site_name"], meta[name="og:site_name"]')?.getAttribute('content') || ''; + let ogUrl = + doc.querySelector('meta[property="og:url"], meta[name="og:url"]')?.getAttribute('content') || originalUrl; + + // fallback: 정규표현식으로 추출 + if (!title) title = extractMetaContent(clean, 'og:title'); + if (!description) description = extractMetaContent(clean, 'og:description'); + if (!image) image = extractMetaContent(clean, 'og:image'); + if (!siteName) siteName = extractMetaContent(clean, 'og:site_name'); + if (!ogUrl) ogUrl = originalUrl; + + // hostname 추출 + let hostname = ''; + try { + hostname = new URL(ogUrl).hostname; + } catch { + hostname = ''; + } + + return { title, description, image, siteName, hostname }; +}; + + +export const useLinkPreviewStore = create((set, get) => ({ + previewMap: new Map(), + loadingMap: new Map(), + getPreview: async (url: string) => { + if (get().previewMap.has(url)) return; + + set(state => { + const loadingMap = new Map(state.loadingMap); + loadingMap.set(url, true); + return { ...state, loadingMap }; + }); + + try { + // 프록시를 통한 요청 URL 구성 + const proxyFetchUrl = proxyUrl + encodeURIComponent(url); + + const response = await fetch(proxyFetchUrl); + const html = await response.text(); + + const parsedData = parseHTML(html, url); + + if (isValidResponse(parsedData)) { + set(state => { + const previewMap = new Map(state.previewMap); + previewMap.set(url, parsedData); + const loadingMap = new Map(state.loadingMap); + loadingMap.set(url, false); + return { ...state, previewMap, loadingMap }; + }); + } else { + set(state => { + const loadingMap = new Map(state.loadingMap); + loadingMap.set(url, false); + return { ...state, loadingMap }; + }); + } + } catch (error) { + console.error('Error fetching link preview:', error); + set(state => { + const loadingMap = new Map(state.loadingMap); + loadingMap.set(url, false); + return { ...state, loadingMap }; + }); + } + }, + getPreviewBatch: async (urls: string[]) => { + for (const url of urls) { + await get().getPreview(url); + } + } +})); \ No newline at end of file diff --git a/apps/website/src/utils/formatKoreanDateTime.ts b/apps/website/src/utils/formatKoreanDateTime.ts index dd7ac2e7..f64758a3 100644 --- a/apps/website/src/utils/formatKoreanDateTime.ts +++ b/apps/website/src/utils/formatKoreanDateTime.ts @@ -24,3 +24,18 @@ export function formatKoreanDateTime(isoString: string): string { return `${year}년 ${month}월 ${day}일 ${hours}:${minutes}`; } + +export function formatKoreanDateTimeNoHour(isoString: string): string { + const date = new Date(isoString); + + if (isNaN(date.getTime())) { + throw new Error("유효하지 않은 날짜 문자열입니다."); + } + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}년 ${month}월 ${day}일`; +} + diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index 1ffef600..5991839a 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -2,6 +2,7 @@ "files": [], "references": [ { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.server.json" } ] } diff --git a/apps/website/tsconfig.server.json b/apps/website/tsconfig.server.json new file mode 100644 index 00000000..7bef6b95 --- /dev/null +++ b/apps/website/tsconfig.server.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", // 기존 웹사이트 tsconfig가 있다면 이어받기 + "compilerOptions": { + "module": "NodeNext", // ESM + CJS 자동 판별 + "moduleResolution": "NodeNext", + "target": "ES2022", + "outDir": "dist", // 선택 + "declaration": false + }, + "include": ["api/**/*.ts", "api/og.js"] + } + \ No newline at end of file diff --git a/package.json b/package.json index 3e20c357..62c8ed54 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,13 @@ }, "dependencies": { "@react-oauth/google": "^0.12.1", + "@sparticuz/chromium": "^138.0.1", + "@sparticuz/chromium-min": "^138.0.1", "@tanstack/react-query": "^5.66.11", "@types/react-router": "^5.1.20", "axios": "^1.8.2", "js-cookie": "^3.0.5", + "puppeteer-core": "^24.14.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54065f05..8de2167b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@react-oauth/google': specifier: ^0.12.1 version: 0.12.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@sparticuz/chromium': + specifier: ^138.0.1 + version: 138.0.1 + '@sparticuz/chromium-min': + specifier: ^138.0.1 + version: 138.0.1 '@tanstack/react-query': specifier: ^5.66.11 version: 5.67.3(react@19.0.0) @@ -23,6 +29,9 @@ importers: js-cookie: specifier: ^3.0.5 version: 3.0.5 + puppeteer-core: + specifier: ^24.14.0 + version: 24.14.0 react: specifier: ^19.0.0 version: 19.0.0 @@ -135,6 +144,9 @@ importers: '@repo/api': specifier: workspace:* version: link:../../packages/api + '@sparticuz/chromium-min': + specifier: ^138.0.1 + version: 138.0.1 '@types/react-slick': specifier: ^0.23.13 version: 0.23.13 @@ -147,6 +159,9 @@ importers: framer-motion: specifier: ^12.4.3 version: 12.5.0(@emotion/is-prop-valid@1.2.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + puppeteer-core: + specifier: ^24.14.0 + version: 24.14.0 react-icons: specifier: ^5.4.0 version: 5.5.0(react@19.0.0) @@ -190,8 +205,11 @@ importers: globals: specifier: ^15.14.0 version: 15.15.0 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.13.10)(typescript@5.6.3) typescript: - specifier: ~5.6.2 + specifier: ~5.6.3 version: 5.6.3 typescript-eslint: specifier: ^8.18.2 @@ -380,6 +398,10 @@ packages: resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==} engines: {node: '>=6.9.0'} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@docsearch/css@3.9.0': resolution: {integrity: sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==} @@ -667,6 +689,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -679,6 +704,11 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@puppeteer/browsers@2.10.6': + resolution: {integrity: sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==} + engines: {node: '>=18'} + hasBin: true + '@react-oauth/google@0.12.1': resolution: {integrity: sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==} peerDependencies: @@ -780,6 +810,14 @@ packages: cpu: [x64] os: [win32] + '@sparticuz/chromium-min@138.0.1': + resolution: {integrity: sha512-Jlwk7tpeMVfjBbqJbcQSYr+OrrwpIGP67gNK5RLkNIuTUNkxW4dmU6fKGsnN3/mLyjzTwEPC1TsZnPGjCoUOGg==} + engines: {node: '>=20.11.0'} + + '@sparticuz/chromium@138.0.1': + resolution: {integrity: sha512-Ub58jeYFc/ePyWvVLqKzGE4/F/nL8KcsUHFNHNxDcqFogGTol4UHiwjZ8gLS4oGfbbT1ZPQHQ9hM3aBrqncBpQ==} + engines: {node: '>=20.11.0'} + '@tanstack/query-core@5.67.3': resolution: {integrity: sha512-pq76ObpjcaspAW4OmCbpXLF6BCZP2Zr/J5ztnyizXhSlNe7fIUp0QKZsd0JMkw9aDa+vxDX/OY7N+hjNY/dCGg==} @@ -788,6 +826,21 @@ packages: peerDependencies: react: ^18 || ^19 + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -847,6 +900,9 @@ packages: '@types/web-bluetooth@0.0.16': resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@8.26.1': resolution: {integrity: sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1006,11 +1062,19 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + acorn@8.14.1: resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1026,18 +1090,62 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} axios@1.8.2: resolution: {integrity: sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==} + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.6.0: + resolution: {integrity: sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==} + + bare-fs@4.1.6: + resolution: {integrity: sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.6.1: + resolution: {integrity: sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.6.5: + resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + body-scroll-lock@4.0.0-beta.0: resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==} @@ -1056,6 +1164,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1074,9 +1185,18 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chromium-bidi@7.1.1: + resolution: {integrity: sha512-L2BKQ0rSLADgbPMIdDh3wnYHs3EiUiMay2Sq0CTolheaADmWIf6Pe+T9LJRcnh5rcMz0U7MVk0cQVvKsGRMa1g==} + peerDependencies: + devtools-protocol: '*' + classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1098,6 +1218,9 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1118,6 +1241,10 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -1127,13 +1254,33 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + devtools-protocol@0.0.1464554: + resolution: {integrity: sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -1145,6 +1292,12 @@ packages: electron-to-chromium@1.5.114: resolution: {integrity: sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enquire.js@2.1.6: resolution: {integrity: sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==} @@ -1302,6 +1455,11 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + eslint-plugin-react-hooks@5.2.0: resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} @@ -1353,6 +1511,11 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -1372,9 +1535,17 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -1388,6 +1559,9 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1457,6 +1631,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1465,6 +1643,14 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1523,6 +1709,14 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + 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'} + hyphenate-style-name@1.1.0: resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} @@ -1545,6 +1739,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -1553,6 +1751,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1582,6 +1784,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1631,9 +1836,16 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + matchmediaquery@0.4.2: resolution: {integrity: sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==} @@ -1664,6 +1876,9 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + motion-dom@12.5.0: resolution: {integrity: sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ==} @@ -1681,6 +1896,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -1703,6 +1922,14 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1725,6 +1952,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1755,16 +1985,31 @@ packages: engines: {node: '>=14'} hasBin: true + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + puppeteer-core@24.14.0: + resolution: {integrity: sha512-NO9XpCl+i8oB0zJp81iPhzMo2QK8/JTj4ramSvTpGCo9CPCNo4AZ8qVOGpSgXzlcOfOT3VHOkzTfPo08GOE5jA==} + engines: {node: '>=18'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1843,6 +2088,10 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -1892,6 +2141,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} @@ -1917,6 +2171,18 @@ packages: peerDependencies: jquery: '>=1.8.0' + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.6: + resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1929,9 +2195,19 @@ packages: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + streamx@2.22.1: + resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} + string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1958,6 +2234,15 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tar-fs@3.1.0: + resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -1971,6 +2256,20 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} @@ -2022,6 +2321,9 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + typed-query-selector@2.12.0: + resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} + typescript-eslint@8.26.1: resolution: {integrity: sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2058,6 +2360,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + vite@3.2.11: resolution: {integrity: sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2196,12 +2501,47 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2449,6 +2789,10 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@docsearch/css@3.9.0': {} '@docsearch/js@3.9.0(@algolia/client-search@5.21.0)(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(search-insights@2.17.3)': @@ -2670,6 +3014,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2682,6 +3031,19 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@puppeteer/browsers@2.10.6': + dependencies: + debug: 4.4.1 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.5.0 + semver: 7.7.2 + tar-fs: 3.1.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bare-buffer + - supports-color + '@react-oauth/google@0.12.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: react: 19.0.0 @@ -2744,6 +3106,22 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.35.0': optional: true + '@sparticuz/chromium-min@138.0.1': + dependencies: + follow-redirects: 1.15.9 + tar-fs: 3.1.0 + transitivePeerDependencies: + - bare-buffer + - debug + + '@sparticuz/chromium@138.0.1': + dependencies: + follow-redirects: 1.15.9 + tar-fs: 3.1.0 + transitivePeerDependencies: + - bare-buffer + - debug + '@tanstack/query-core@5.67.3': {} '@tanstack/react-query@5.67.3(react@19.0.0)': @@ -2751,6 +3129,16 @@ snapshots: '@tanstack/query-core': 5.67.3 react: 19.0.0 + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.10 @@ -2824,6 +3212,11 @@ snapshots: '@types/web-bluetooth@0.0.16': {} + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.13.10 + optional: true + '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.6.3))(eslint@9.22.0)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3093,8 +3486,14 @@ snapshots: dependencies: acorn: 8.14.1 + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.1 + acorn@8.14.1: {} + agent-base@7.1.4: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -3124,8 +3523,14 @@ snapshots: dependencies: color-convert: 2.0.1 + arg@4.1.3: {} + argparse@2.0.1: {} + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + asynckit@0.4.0: {} axios@1.8.2: @@ -3136,8 +3541,37 @@ snapshots: transitivePeerDependencies: - debug + b4a@1.6.7: {} + balanced-match@1.0.2: {} + bare-events@2.6.0: + optional: true + + bare-fs@4.1.6: + dependencies: + bare-events: 2.6.0 + bare-path: 3.0.0 + bare-stream: 2.6.5(bare-events@2.6.0) + optional: true + + bare-os@3.6.1: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.1 + optional: true + + bare-stream@2.6.5(bare-events@2.6.0): + dependencies: + streamx: 2.22.1 + optionalDependencies: + bare-events: 2.6.0 + optional: true + + basic-ftp@5.0.5: {} + body-scroll-lock@4.0.0-beta.0: {} brace-expansion@1.1.11: @@ -3160,6 +3594,8 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) + buffer-crc32@0.2.13: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -3176,8 +3612,20 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chromium-bidi@7.1.1(devtools-protocol@0.0.1464554): + dependencies: + devtools-protocol: 0.0.1464554 + mitt: 3.0.1 + zod: 3.24.2 + classnames@2.5.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3194,6 +3642,8 @@ snapshots: cookie@1.0.2: {} + create-require@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3214,14 +3664,30 @@ snapshots: csstype@3.1.3: {} + data-uri-to-buffer@6.0.2: {} + debug@4.4.0: dependencies: ms: 2.1.3 + debug@4.4.1: + dependencies: + ms: 2.1.3 + deep-is@0.1.4: {} + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + delayed-stream@1.0.0: {} + devtools-protocol@0.0.1464554: {} + + diff@4.0.2: {} + doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -3234,6 +3700,12 @@ snapshots: electron-to-chromium@1.5.114: {} + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enquire.js@2.1.6: {} es-define-property@1.0.1: {} @@ -3368,6 +3840,14 @@ snapshots: escape-string-regexp@4.0.0: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + eslint-plugin-react-hooks@5.2.0(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -3493,6 +3973,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 3.4.3 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -3507,8 +3989,20 @@ snapshots: esutils@2.0.3: {} + extract-zip@2.0.1: + dependencies: + debug: 4.4.1 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3525,6 +4019,10 @@ snapshots: dependencies: reusify: 1.1.0 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -3583,6 +4081,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3601,6 +4101,18 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -3650,6 +4162,20 @@ snapshots: dependencies: react-is: 16.13.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + hyphenate-style-name@1.1.0: {} ignore@5.3.2: {} @@ -3668,12 +4194,19 @@ snapshots: inherits@2.0.4: {} + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + is-core-module@2.16.1: dependencies: hasown: 2.0.2 is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3694,6 +4227,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsbn@1.1.0: {} + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -3735,10 +4270,14 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@7.18.3: {} + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 + make-error@1.3.6: {} + matchmediaquery@0.4.2: dependencies: css-mediaquery: 0.1.2 @@ -3766,6 +4305,8 @@ snapshots: dependencies: brace-expansion: 2.0.1 + mitt@3.0.1: {} + motion-dom@12.5.0: dependencies: motion-utils: 12.5.0 @@ -3778,6 +4319,8 @@ snapshots: natural-compare@1.4.0: {} + netmask@2.0.2: {} + node-releases@2.0.19: {} object-assign@4.1.1: {} @@ -3803,6 +4346,24 @@ snapshots: dependencies: p-limit: 3.1.0 + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.1 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -3817,6 +4378,8 @@ snapshots: path-parse@1.0.7: {} + pend@1.2.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3841,16 +4404,50 @@ snapshots: prettier@3.5.3: {} + progress@2.0.3: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + proxy-from-env@1.1.0: {} + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} + puppeteer-core@24.14.0: + dependencies: + '@puppeteer/browsers': 2.10.6 + chromium-bidi: 7.1.1(devtools-protocol@0.0.1464554) + debug: 4.4.1 + devtools-protocol: 0.0.1464554 + typed-query-selector: 2.12.0 + ws: 8.18.3 + transitivePeerDependencies: + - bare-buffer + - bufferutil + - supports-color + - utf-8-validate + queue-microtask@1.2.3: {} react-dom@19.0.0(react@19.0.0): @@ -3926,6 +4523,8 @@ snapshots: react@19.0.0: {} + require-directory@2.1.1: {} + resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} @@ -3983,6 +4582,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.2: {} + set-cookie-parser@2.7.1: {} shallow-equal@3.1.0: {} @@ -4005,14 +4606,44 @@ snapshots: dependencies: jquery: 3.7.1 + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + socks: 2.8.6 + transitivePeerDependencies: + - supports-color + + socks@2.8.6: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} source-map@0.6.1: {} sourcemap-codec@1.4.8: {} + sprintf-js@1.1.3: {} + + streamx@2.22.1: + dependencies: + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.6.0 + string-convert@0.2.1: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4041,6 +4672,26 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tar-fs@3.1.0: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.1.6 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-buffer + + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.22.1 + + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 + text-table@0.2.0: {} to-regex-range@5.0.1: @@ -4055,6 +4706,24 @@ snapshots: dependencies: typescript: 5.8.2 + ts-node@10.9.2(@types/node@22.13.10)(typescript@5.6.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.13.10 + acorn: 8.14.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + tslib@2.6.2: {} tslib@2.8.1: {} @@ -4094,6 +4763,8 @@ snapshots: type-fest@0.20.2: {} + typed-query-selector@2.12.0: {} + typescript-eslint@8.26.1(eslint@9.22.0)(typescript@5.6.3): dependencies: '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.6.3))(eslint@9.22.0)(typescript@5.6.3) @@ -4130,6 +4801,8 @@ snapshots: dependencies: punycode: 2.3.1 + v8-compile-cache-lib@3.0.1: {} + vite@3.2.11(@types/node@22.13.10): dependencies: esbuild: 0.15.18 @@ -4205,10 +4878,39 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrappy@1.0.2: {} + ws@8.18.3: {} + + y18n@5.0.8: {} + yallist@3.1.1: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yn@3.1.1: {} + yocto-queue@0.1.0: {} zod@3.24.2: {} diff --git a/turbo.json b/turbo.json index dcf4a87f..2310c1b3 100644 --- a/turbo.json +++ b/turbo.json @@ -23,6 +23,10 @@ ".env.local", ".env.production", ".env" + ], + "outputs": [ + "dist/**", + ".vitepress/dist/**" ] }, "lint": {