diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index e215bc4..0000000 --- a/docs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/docs/next.config.ts b/docs/next.config.ts index 6a5d444..c453c5e 100644 --- a/docs/next.config.ts +++ b/docs/next.config.ts @@ -6,14 +6,23 @@ const withNextra = nextra({ search: { codeblocks: false, }, - contentDirBasePath: "/docs", + readingTime: true, }); const nextConfig: NextConfig = { + reactStrictMode: true, i18n: { locales: ["en", "ko"], defaultLocale: "en", }, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "contrib.rocks", + }, + ], + }, }; export default withNextra(nextConfig); diff --git a/docs/package.json b/docs/package.json index dd03668..b169772 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", @@ -12,23 +12,28 @@ }, "dependencies": { "@amplitude/analytics-browser": "^2.11.11", - "@offlegacy/event-tracker": "^1.0.3", - "next": "15.1.6", - "nextra": "^4.0.7", - "nextra-theme-docs": "^4.0.7", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "@offlegacy/event-tracker": "latest", + "@uidotdev/usehooks": "^2.4.1", + "codehike": "^1.0.7", + "lucide-react": "^0.525.0", + "motion": "^12.23.0", + "next": "15.3.5", + "nextra": "^4.2.17", + "nextra-theme-docs": "^4.2.17", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "sonner": "^2.0.6" }, "devDependencies": { - "@eslint/eslintrc": "^3.2.0", + "@eslint/eslintrc": "^3.3.1", "@types/node": "^20", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", "eslint": "^9", - "eslint-config-next": "15.1.6", + "eslint-config-next": "15.3.5", "pagefind": "^1.3.0", "postcss": "^8", - "prettier-plugin-tailwindcss": "^0.6.11", + "prettier-plugin-tailwindcss": "^0.6.13", "tailwindcss": "^3.4.1", "typescript": "^5" } diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index cf56380..6324f42 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -12,42 +12,57 @@ importers: specifier: ^2.11.11 version: 2.11.11 '@offlegacy/event-tracker': - specifier: ^1.0.3 - version: 1.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: latest + version: 1.3.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(zod@3.24.1) + '@uidotdev/usehooks': + specifier: ^2.4.1 + version: 2.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + codehike: + specifier: ^1.0.7 + version: 1.0.7 + lucide-react: + specifier: ^0.525.0 + version: 0.525.0(react@19.1.1) + motion: + specifier: ^12.23.0 + version: 12.23.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next: - specifier: 15.1.6 - version: 15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 15.3.5 + version: 15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) nextra: - specifier: ^4.0.7 - version: 4.0.7(acorn@8.14.0)(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) + specifier: ^4.2.17 + version: 4.2.17(acorn@8.14.0)(next@15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.3) nextra-theme-docs: - specifier: ^4.0.7 - version: 4.0.7(@types/react@19.0.8)(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.7(acorn@8.14.0)(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^4.2.17 + version: 4.2.17(@types/react@19.1.9)(next@15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nextra@4.2.17(acorn@8.14.0)(next@15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: - specifier: ^19.0.0 - version: 19.0.0 + specifier: ^19.1.1 + version: 19.1.1 react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) + specifier: ^19.1.1 + version: 19.1.1(react@19.1.1) + sonner: + specifier: ^2.0.6 + version: 2.0.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) devDependencies: '@eslint/eslintrc': - specifier: ^3.2.0 - version: 3.2.0 + specifier: ^3.3.1 + version: 3.3.1 '@types/node': specifier: ^20 - version: 20.17.16 + version: 20.19.9 '@types/react': - specifier: ^19.0.0 - version: 19.0.8 + specifier: ^19.1.9 + version: 19.1.9 '@types/react-dom': - specifier: ^19.0.0 - version: 19.0.3(@types/react@19.0.8) + specifier: ^19.1.7 + version: 19.1.7(@types/react@19.1.9) eslint: specifier: ^9 version: 9.18.0(jiti@1.21.7) eslint-config-next: - specifier: 15.1.6 - version: 15.1.6(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3) + specifier: 15.3.5 + version: 15.3.5(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3) pagefind: specifier: ^1.3.0 version: 1.3.0 @@ -55,8 +70,8 @@ importers: specifier: ^8 version: 8.5.1 prettier-plugin-tailwindcss: - specifier: ^0.6.11 - version: 0.6.11(prettier@3.4.2) + specifier: ^0.6.13 + version: 0.6.13(prettier@3.4.2) tailwindcss: specifier: ^3.4.1 version: 3.4.17 @@ -121,8 +136,11 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@emnapi/runtime@1.3.1': - resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@code-hike/lighter@1.0.1': + resolution: {integrity: sha512-mccvcsk5UTScRrE02oBz1/qzckyhD8YE3VQlQv++2bSVVZgNuCUX8MpokSCi5OmfRAAxbj6kmNiqq1Um8eXPrw==} + + '@emnapi/runtime@1.4.4': + resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} @@ -142,8 +160,8 @@ packages: resolution: {integrity: sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.2.0': - resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.18.0': @@ -179,8 +197,8 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} - '@formatjs/intl-localematcher@0.5.10': - resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==} + '@formatjs/intl-localematcher@0.6.1': + resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==} '@headlessui/react@2.2.0': resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==} @@ -215,107 +233,118 @@ packages: '@iconify/utils@2.2.1': resolution: {integrity: sha512-0/7J7hk4PqXmxo5PDBDxmnecw5PxklZJfNjIVG9FM0mEfVrvfudS22rYWsqVk6gR3UJ/mSYS90X4R3znXnqfNA==} - '@img/sharp-darwin-arm64@0.33.5': - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + '@img/sharp-darwin-arm64@0.34.2': + resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + '@img/sharp-darwin-x64@0.34.2': + resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + '@img/sharp-libvips-darwin-arm64@1.1.0': + resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.0.4': - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + '@img/sharp-libvips-darwin-x64@1.1.0': + resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.0.4': - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + '@img/sharp-libvips-linux-arm64@1.1.0': + resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.0.5': - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + '@img/sharp-libvips-linux-arm@1.1.0': + resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-s390x@1.0.4': - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + '@img/sharp-libvips-linux-ppc64@1.1.0': + resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.1.0': + resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.0.4': - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + '@img/sharp-libvips-linux-x64@1.1.0': + resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.33.5': - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + '@img/sharp-linux-arm64@0.34.2': + resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + '@img/sharp-linux-arm@0.34.2': + resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + '@img/sharp-linux-s390x@0.34.2': + resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + '@img/sharp-linux-x64@0.34.2': + resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + '@img/sharp-linuxmusl-arm64@0.34.2': + resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + '@img/sharp-linuxmusl-x64@0.34.2': + resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + '@img/sharp-wasm32@0.34.2': + resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + '@img/sharp-win32-arm64@0.34.2': + resolution: {integrity: sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.2': + resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + '@img/sharp-win32-x64@0.34.2': + resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -436,56 +465,56 @@ packages: resolution: {integrity: sha512-jMxvwzkKzd3cXo2EB9GM2ic0eYo2rP/BS6gJt6HnWbsDO1O8GSD4k7o2Cpr2YERtMpGF/MGcDfsfj2EbQPtrXw==} engines: {node: '>= 10'} - '@next/env@15.1.6': - resolution: {integrity: sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==} + '@next/env@15.3.5': + resolution: {integrity: sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g==} - '@next/eslint-plugin-next@15.1.6': - resolution: {integrity: sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==} + '@next/eslint-plugin-next@15.3.5': + resolution: {integrity: sha512-BZwWPGfp9po/rAnJcwUBaM+yT/+yTWIkWdyDwc74G9jcfTrNrmsHe+hXHljV066YNdVs8cxROxX5IgMQGX190w==} - '@next/swc-darwin-arm64@15.1.6': - resolution: {integrity: sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==} + '@next/swc-darwin-arm64@15.3.5': + resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.1.6': - resolution: {integrity: sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==} + '@next/swc-darwin-x64@15.3.5': + resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.1.6': - resolution: {integrity: sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==} + '@next/swc-linux-arm64-gnu@15.3.5': + resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.1.6': - resolution: {integrity: sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==} + '@next/swc-linux-arm64-musl@15.3.5': + resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.1.6': - resolution: {integrity: sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==} + '@next/swc-linux-x64-gnu@15.3.5': + resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.1.6': - resolution: {integrity: sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==} + '@next/swc-linux-x64-musl@15.3.5': + resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.1.6': - resolution: {integrity: sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==} + '@next/swc-win32-arm64-msvc@15.3.5': + resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.1.6': - resolution: {integrity: sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==} + '@next/swc-win32-x64-msvc@15.3.5': + resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -506,11 +535,12 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} - '@offlegacy/event-tracker@1.0.3': - resolution: {integrity: sha512-RAbZgPRSk3NDe6hUa3GrezmBCf4pFe7H6XoAqE0u0Y1tnZugk+9Lx1ImC0yT6sERCtG9kPHlWPNpTv4WzsOiWQ==} + '@offlegacy/event-tracker@1.3.0': + resolution: {integrity: sha512-js1f8wBkNH7LAorneda0BAW3pElfE9EmhJTmUJgB8j7Q5ccWrTUdP9pjPK28bOPA7r4FNaoQojunN6m49Cd7Dg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + zod: ^3.20.0 '@pagefind/darwin-arm64@1.3.0': resolution: {integrity: sha512-365BEGl6ChOsauRjyVpBjXybflXAOvoMROw3TucAROHIcdBvXk9/2AmEvGFU0r75+vdQI4LJdJdpH4Y6Yqaj4A==} @@ -581,29 +611,29 @@ packages: '@rushstack/eslint-patch@1.10.5': resolution: {integrity: sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==} - '@shikijs/core@1.29.1': - resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==} + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} - '@shikijs/engine-javascript@1.29.1': - resolution: {integrity: sha512-Hpi8k9x77rCQ7F/7zxIOUruNkNidMyBnP5qAGbLFqg4kRrg1HZhkB8btib5EXbQWTtLb5gBHOdBwshk20njD7Q==} + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} - '@shikijs/engine-oniguruma@1.29.1': - resolution: {integrity: sha512-gSt2WhLNgEeLstcweQOSp+C+MhOpTsgdNXRqr3zP6M+BUBZ8Md9OU2BYwUYsALBxHza7hwaIWtFHjQ/aOOychw==} + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} - '@shikijs/langs@1.29.1': - resolution: {integrity: sha512-iERn4HlyuT044/FgrvLOaZgKVKf3PozjKjyV/RZ5GnlyYEAZFcgwHGkYboeBv2IybQG1KVS/e7VGgiAU4JY2Gw==} + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} - '@shikijs/themes@1.29.1': - resolution: {integrity: sha512-lb11zf72Vc9uxkl+aec2oW1HVTHJ2LtgZgumb4Rr6By3y/96VmlU44bkxEb8WBWH3RUtbqAJEN0jljD9cF7H7g==} + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} - '@shikijs/twoslash@1.29.1': - resolution: {integrity: sha512-SN2aam87NjkpjS0O2Zq9SeXSDX1CztLBAROXrJpEe5Qe19dkMUUXY8uhw32Qu/FKjqsK8ycEP2S6FZrd9A2pzw==} + '@shikijs/twoslash@2.5.0': + resolution: {integrity: sha512-OdyoZRbzTB80qHFHdaXT070OG9hiljxbsJMZmrMAPWXG2e4FV8wbC63VBM5BJXa1DH645nw20VX1MzASkO5V9g==} - '@shikijs/types@1.29.1': - resolution: {integrity: sha512-aBqAuhYRp5vSir3Pc9+QPu9WESBOjUo03ao0IHLC4TyTioSsp/SkbAZSrIH4ghYYC1T1KTEpRSBa83bas4RnPA==} + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} - '@shikijs/vscode-textmate@10.0.1': - resolution: {integrity: sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==} + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -760,16 +790,16 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} - '@types/node@20.17.16': - resolution: {integrity: sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==} + '@types/node@20.19.9': + resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} - '@types/react-dom@19.0.3': - resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} + '@types/react-dom@19.1.7': + resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==} peerDependencies: '@types/react': ^19.0.0 - '@types/react@19.0.8': - resolution: {integrity: sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==} + '@types/react@19.1.9': + resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==} '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -832,6 +862,13 @@ packages: peerDependencies: typescript: '*' + '@uidotdev/usehooks@2.4.1': + resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} + engines: {node: '>=16'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -856,6 +893,9 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} + ansi-sequence-parser@1.1.1: + resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1037,6 +1077,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codehike@1.0.7: + resolution: {integrity: sha512-m4YOQv1l06qoGBLCgXRNT45MKAudXjir9v+WTs7Xb0gfze6LN3ZiUp08+WRA1N/QGT1SHbzvck8txvPq+C2EPg==} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -1312,8 +1355,8 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} devlop@1.1.0: @@ -1322,6 +1365,10 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -1401,8 +1448,8 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@15.1.6: - resolution: {integrity: sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg==} + eslint-config-next@15.3.5: + resolution: {integrity: sha512-oQdvnIgP68wh2RlR3MdQpvaJ94R6qEFl+lnu8ZKxPj5fsAHrSF/HlAOZcsimLw3DT6bnEQIUdbZC2Ab6sWyptg==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' @@ -1612,6 +1659,20 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + framer-motion@12.23.0: + resolution: {integrity: sha512-xf6NxTGAyf7zR4r2KlnhFmsRfKIbjqeBupEDBAaEtVIBJX96sAon00kMlsKButSIRwPSHjbRrAPnYdJJ9kyhbA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2056,6 +2117,11 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lucide-react@0.525.0: + resolution: {integrity: sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} @@ -2281,6 +2347,26 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + motion-dom@12.22.0: + resolution: {integrity: sha512-ooH7+/BPw9gOsL9VtPhEJHE2m4ltnhMlcGMhEqA0YGNhKof7jdaszvsyThXI6LVIKshJUZ9/CP6HNqQhJfV7kw==} + + motion-utils@12.19.0: + resolution: {integrity: sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==} + + motion@12.23.0: + resolution: {integrity: sha512-PPNwblArRH9GRC4F3KtOTiIaYd+mtp324vYq3HIL+ueseoAVqPRK5TPFTAQBcIprfVd0NWo3DLzZSiyWaYFXXQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2305,8 +2391,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.1.6: - resolution: {integrity: sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==} + next@15.3.5: + resolution: {integrity: sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -2326,16 +2412,16 @@ packages: sass: optional: true - nextra-theme-docs@4.0.7: - resolution: {integrity: sha512-KZMphMeN8r4mRVczDwlmIfDAzFyfYdJyE4TWqx9OblybazMD8gwHSC7nZAidLZSLzwWEUrf8vnBWN4KcXqyM8w==} + nextra-theme-docs@4.2.17: + resolution: {integrity: sha512-QQ7iPHQ7zqh7dKJZ3SQbxqoFt7r8RHD9v7dFtJ+x8evEfxwM23oBGHNBqIjBoPl5uSwtvufEiVd7WMhK+Dxdww==} peerDependencies: next: '>=14' - nextra: 4.0.7 + nextra: 4.2.17 react: '>=18' react-dom: '>=18' - nextra@4.0.7: - resolution: {integrity: sha512-jf+jpJoAE//bS0xZn0tn7fAGascdyI4U/JBj0kiI53gUsdcXV3j5/Lw6n/XLoeoPM0ptfb5WbVP2wrKIwNX59g==} + nextra@4.2.17: + resolution: {integrity: sha512-WBZGSUeUJqkYm3F3F7+4N1oMP84r/YK/rAg96wkywu/MIsuUREY8fLXQgQbKkvcLbBl/7Wk2Iy+9xlhDu+weNg==} engines: {node: '>=18'} peerDependencies: next: '>=14' @@ -2397,8 +2483,8 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} - oniguruma-to-es@2.3.0: - resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==} + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -2544,8 +2630,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-tailwindcss@0.6.11: - resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==} + prettier-plugin-tailwindcss@0.6.13: + resolution: {integrity: sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==} engines: {node: '>=14.21.3'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' @@ -2622,10 +2708,10 @@ packages: peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom@19.0.0: - resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + react-dom@19.1.1: + resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: - react: ^19.0.0 + react: ^19.1.1 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2636,8 +2722,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react@19.0.0: - resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -2666,14 +2752,14 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regex-recursion@5.1.1: - resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} regex-utilities@2.3.0: resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - regex@5.1.1: - resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==} + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} @@ -2685,11 +2771,11 @@ packages: rehype-parse@9.0.1: resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} - rehype-pretty-code@0.14.0: - resolution: {integrity: sha512-hBeKF/Wkkf3zyUS8lal9RCUuhypDWLQc+h9UrP9Pav25FUm/AQAVh4m5gdvJxh4Oz+U+xKvdsV01p1LdvsZTiQ==} + rehype-pretty-code@0.14.1: + resolution: {integrity: sha512-IpG4OL0iYlbx78muVldsK86hdfNoht0z63AP7sekQNW2QOTmjxB7RbTO+rhIYNGRljgHxgVZoPwUl6bIC9SbjA==} engines: {node: '>=18'} peerDependencies: - shiki: ^1.3.0 + shiki: ^1.0.0 || ^2.0.0 || ^3.0.0 rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} @@ -2787,8 +2873,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.25.0: - resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} @@ -2802,6 +2888,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-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2814,8 +2905,8 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + sharp@0.34.2: + resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -2826,8 +2917,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@1.29.1: - resolution: {integrity: sha512-TghWKV9pJTd/N+IgAIVJtr0qZkB7FfFCUrrEJc0aRmZupo3D1OCVRknQWVRVA7AX/M0Ld7QfoAruPzr3CnUJuw==} + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} @@ -2856,6 +2947,12 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + sonner@2.0.6: + resolution: {integrity: sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3063,8 +3160,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -3314,7 +3411,11 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@emnapi/runtime@1.3.1': + '@code-hike/lighter@1.0.1': + dependencies: + ansi-sequence-parser: 1.1.1 + + '@emnapi/runtime@1.4.4': dependencies: tslib: 2.8.1 optional: true @@ -3338,7 +3439,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.2.0': + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 debug: 4.4.0 @@ -3370,34 +3471,34 @@ snapshots: '@floating-ui/core': 1.6.9 '@floating-ui/utils': 0.2.9 - '@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@floating-ui/react-dom@2.1.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@floating-ui/dom': 1.6.13 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - '@floating-ui/react@0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@floating-ui/react@0.26.28(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@floating-ui/react-dom': 2.1.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@floating-ui/utils': 0.2.9 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) tabbable: 6.2.0 '@floating-ui/utils@0.2.9': {} - '@formatjs/intl-localematcher@0.5.10': + '@formatjs/intl-localematcher@0.6.1': dependencies: tslib: 2.8.1 - '@headlessui/react@2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@headlessui/react@2.2.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@floating-ui/react': 0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-aria/focus': 3.19.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-aria/interactions': 3.23.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@tanstack/react-virtual': 3.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@floating-ui/react': 0.26.28(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@react-aria/focus': 3.19.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@react-aria/interactions': 3.23.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@tanstack/react-virtual': 3.11.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) '@humanfs/core@0.19.1': {} @@ -3427,79 +3528,85 @@ snapshots: transitivePeerDependencies: - supports-color - '@img/sharp-darwin-arm64@0.33.5': + '@img/sharp-darwin-arm64@0.34.2': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-arm64': 1.1.0 optional: true - '@img/sharp-darwin-x64@0.33.5': + '@img/sharp-darwin-x64@0.34.2': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.1.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.1.0': optional: true - '@img/sharp-libvips-darwin-arm64@1.0.4': + '@img/sharp-libvips-darwin-x64@1.1.0': optional: true - '@img/sharp-libvips-darwin-x64@1.0.4': + '@img/sharp-libvips-linux-arm64@1.1.0': optional: true - '@img/sharp-libvips-linux-arm64@1.0.4': + '@img/sharp-libvips-linux-arm@1.1.0': optional: true - '@img/sharp-libvips-linux-arm@1.0.5': + '@img/sharp-libvips-linux-ppc64@1.1.0': optional: true - '@img/sharp-libvips-linux-s390x@1.0.4': + '@img/sharp-libvips-linux-s390x@1.1.0': optional: true - '@img/sharp-libvips-linux-x64@1.0.4': + '@img/sharp-libvips-linux-x64@1.1.0': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.0.4': + '@img/sharp-libvips-linuxmusl-x64@1.1.0': optional: true - '@img/sharp-linux-arm64@0.33.5': + '@img/sharp-linux-arm64@0.34.2': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-arm64': 1.1.0 optional: true - '@img/sharp-linux-arm@0.33.5': + '@img/sharp-linux-arm@0.34.2': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm': 1.1.0 optional: true - '@img/sharp-linux-s390x@0.33.5': + '@img/sharp-linux-s390x@0.34.2': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.1.0 optional: true - '@img/sharp-linux-x64@0.33.5': + '@img/sharp-linux-x64@0.34.2': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.1.0 optional: true - '@img/sharp-linuxmusl-arm64@0.33.5': + '@img/sharp-linuxmusl-arm64@0.34.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 optional: true - '@img/sharp-linuxmusl-x64@0.33.5': + '@img/sharp-linuxmusl-x64@0.34.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 optional: true - '@img/sharp-wasm32@0.33.5': + '@img/sharp-wasm32@0.34.2': dependencies: - '@emnapi/runtime': 1.3.1 + '@emnapi/runtime': 1.4.4 optional: true - '@img/sharp-win32-ia32@0.33.5': + '@img/sharp-win32-arm64@0.34.2': optional: true - '@img/sharp-win32-x64@0.33.5': + '@img/sharp-win32-ia32@0.34.2': + optional: true + + '@img/sharp-win32-x64@0.34.2': optional: true '@isaacs/cliui@8.0.2': @@ -3621,34 +3728,34 @@ snapshots: '@napi-rs/simple-git-win32-arm64-msvc': 0.1.19 '@napi-rs/simple-git-win32-x64-msvc': 0.1.19 - '@next/env@15.1.6': {} + '@next/env@15.3.5': {} - '@next/eslint-plugin-next@15.1.6': + '@next/eslint-plugin-next@15.3.5': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.1.6': + '@next/swc-darwin-arm64@15.3.5': optional: true - '@next/swc-darwin-x64@15.1.6': + '@next/swc-darwin-x64@15.3.5': optional: true - '@next/swc-linux-arm64-gnu@15.1.6': + '@next/swc-linux-arm64-gnu@15.3.5': optional: true - '@next/swc-linux-arm64-musl@15.1.6': + '@next/swc-linux-arm64-musl@15.3.5': optional: true - '@next/swc-linux-x64-gnu@15.1.6': + '@next/swc-linux-x64-gnu@15.3.5': optional: true - '@next/swc-linux-x64-musl@15.1.6': + '@next/swc-linux-x64-musl@15.3.5': optional: true - '@next/swc-win32-arm64-msvc@15.1.6': + '@next/swc-win32-arm64-msvc@15.3.5': optional: true - '@next/swc-win32-x64-msvc@15.1.6': + '@next/swc-win32-x64-msvc@15.3.5': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3665,10 +3772,11 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@offlegacy/event-tracker@1.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@offlegacy/event-tracker@1.3.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(zod@3.24.1)': dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + zod: 3.24.1 '@pagefind/darwin-arm64@1.3.0': optional: true @@ -3688,96 +3796,96 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@react-aria/focus@3.19.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-aria/focus@3.19.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@react-aria/interactions': 3.23.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-aria/utils': 3.27.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-types/shared': 3.27.0(react@19.0.0) + '@react-aria/interactions': 3.23.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@react-aria/utils': 3.27.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@react-types/shared': 3.27.0(react@19.1.1) '@swc/helpers': 0.5.15 clsx: 2.1.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - '@react-aria/interactions@3.23.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-aria/interactions@3.23.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@react-aria/ssr': 3.9.7(react@19.0.0) - '@react-aria/utils': 3.27.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-types/shared': 3.27.0(react@19.0.0) + '@react-aria/ssr': 3.9.7(react@19.1.1) + '@react-aria/utils': 3.27.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@react-types/shared': 3.27.0(react@19.1.1) '@swc/helpers': 0.5.15 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - '@react-aria/ssr@3.9.7(react@19.0.0)': + '@react-aria/ssr@3.9.7(react@19.1.1)': dependencies: '@swc/helpers': 0.5.15 - react: 19.0.0 + react: 19.1.1 - '@react-aria/utils@3.27.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-aria/utils@3.27.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@react-aria/ssr': 3.9.7(react@19.0.0) - '@react-stately/utils': 3.10.5(react@19.0.0) - '@react-types/shared': 3.27.0(react@19.0.0) + '@react-aria/ssr': 3.9.7(react@19.1.1) + '@react-stately/utils': 3.10.5(react@19.1.1) + '@react-types/shared': 3.27.0(react@19.1.1) '@swc/helpers': 0.5.15 clsx: 2.1.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - '@react-stately/utils@3.10.5(react@19.0.0)': + '@react-stately/utils@3.10.5(react@19.1.1)': dependencies: '@swc/helpers': 0.5.15 - react: 19.0.0 + react: 19.1.1 - '@react-types/shared@3.27.0(react@19.0.0)': + '@react-types/shared@3.27.0(react@19.1.1)': dependencies: - react: 19.0.0 + react: 19.1.1 '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.10.5': {} - '@shikijs/core@1.29.1': + '@shikijs/core@2.5.0': dependencies: - '@shikijs/engine-javascript': 1.29.1 - '@shikijs/engine-oniguruma': 1.29.1 - '@shikijs/types': 1.29.1 - '@shikijs/vscode-textmate': 10.0.1 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 hast-util-to-html: 9.0.4 - '@shikijs/engine-javascript@1.29.1': + '@shikijs/engine-javascript@2.5.0': dependencies: - '@shikijs/types': 1.29.1 - '@shikijs/vscode-textmate': 10.0.1 - oniguruma-to-es: 2.3.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 - '@shikijs/engine-oniguruma@1.29.1': + '@shikijs/engine-oniguruma@2.5.0': dependencies: - '@shikijs/types': 1.29.1 - '@shikijs/vscode-textmate': 10.0.1 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@1.29.1': + '@shikijs/langs@2.5.0': dependencies: - '@shikijs/types': 1.29.1 + '@shikijs/types': 2.5.0 - '@shikijs/themes@1.29.1': + '@shikijs/themes@2.5.0': dependencies: - '@shikijs/types': 1.29.1 + '@shikijs/types': 2.5.0 - '@shikijs/twoslash@1.29.1(typescript@5.7.3)': + '@shikijs/twoslash@2.5.0(typescript@5.7.3)': dependencies: - '@shikijs/core': 1.29.1 - '@shikijs/types': 1.29.1 + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 twoslash: 0.2.12(typescript@5.7.3) transitivePeerDependencies: - supports-color - typescript - '@shikijs/types@1.29.1': + '@shikijs/types@2.5.0': dependencies: - '@shikijs/vscode-textmate': 10.0.1 + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/vscode-textmate@10.0.1': {} + '@shikijs/vscode-textmate@10.0.2': {} '@swc/counter@0.1.3': {} @@ -3785,18 +3893,18 @@ snapshots: dependencies: tslib: 2.8.1 - '@tanstack/react-virtual@3.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/react-virtual@3.11.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@tanstack/virtual-core': 3.11.2 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) '@tanstack/virtual-core@3.11.2': {} - '@theguild/remark-mermaid@0.2.0(react@19.0.0)': + '@theguild/remark-mermaid@0.2.0(react@19.1.1)': dependencies: mermaid: 11.4.1 - react: 19.0.0 + react: 19.1.1 unist-util-visit: 5.0.0 transitivePeerDependencies: - supports-color @@ -3961,15 +4069,15 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/node@20.17.16': + '@types/node@20.19.9': dependencies: - undici-types: 6.19.8 + undici-types: 6.21.0 - '@types/react-dom@19.0.3(@types/react@19.0.8)': + '@types/react-dom@19.1.7(@types/react@19.1.9)': dependencies: - '@types/react': 19.0.8 + '@types/react': 19.1.9 - '@types/react@19.0.8': + '@types/react@19.1.9': dependencies: csstype: 3.1.3 @@ -4064,6 +4172,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@uidotdev/usehooks@2.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + '@ungap/structured-clone@1.3.0': {} acorn-jsx@5.3.2(acorn@8.14.0): @@ -4083,6 +4196,8 @@ snapshots: ansi-regex@6.1.0: {} + ansi-sequence-parser@1.1.1: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -4186,10 +4301,10 @@ snapshots: balanced-match@1.0.2: {} - better-react-mathjax@2.0.3(react@19.0.0): + better-react-mathjax@2.0.3(react@19.1.1): dependencies: mathjax-full: 3.2.2 - react: 19.0.0 + react: 19.1.1 binary-extensions@2.3.0: {} @@ -4286,6 +4401,16 @@ snapshots: clsx@2.1.1: {} + codehike@1.0.7: + dependencies: + '@code-hike/lighter': 1.0.1 + diff: 5.2.0 + estree-util-visit: 2.0.0 + mdast-util-mdx-jsx: 3.2.0 + unist-util-visit: 5.0.0 + transitivePeerDependencies: + - supports-color + collapse-white-space@2.1.0: {} color-convert@2.0.1: @@ -4578,7 +4703,7 @@ snapshots: dequal@2.0.3: {} - detect-libc@2.0.3: + detect-libc@2.0.4: optional: true devlop@1.1.0: @@ -4587,6 +4712,8 @@ snapshots: didyoumean@1.2.2: {} + diff@5.2.0: {} + dlv@1.1.3: {} doctrine@2.1.0: @@ -4734,9 +4861,9 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@15.1.6(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3): + eslint-config-next@15.3.5(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3): dependencies: - '@next/eslint-plugin-next': 15.1.6 + '@next/eslint-plugin-next': 15.3.5 '@rushstack/eslint-patch': 1.10.5 '@typescript-eslint/eslint-plugin': 8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3) '@typescript-eslint/parser': 8.21.0(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3) @@ -4878,7 +5005,7 @@ snapshots: '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.1 '@eslint/core': 0.10.0 - '@eslint/eslintrc': 3.2.0 + '@eslint/eslintrc': 3.3.1 '@eslint/js': 9.18.0 '@eslint/plugin-kit': 0.2.5 '@humanfs/node': 0.16.6 @@ -5051,6 +5178,15 @@ snapshots: format@0.2.2: {} + framer-motion@12.23.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + motion-dom: 12.22.0 + motion-utils: 12.19.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + fsevents@2.3.3: optional: true @@ -5584,6 +5720,10 @@ snapshots: lru-cache@10.4.3: {} + lucide-react@0.525.0(react@19.1.1): + dependencies: + react: 19.1.1 + markdown-extensions@2.0.0: {} markdown-table@3.0.4: {} @@ -6127,6 +6267,20 @@ snapshots: pkg-types: 1.3.1 ufo: 1.5.4 + motion-dom@12.22.0: + dependencies: + motion-utils: 12.19.0 + + motion-utils@12.19.0: {} + + motion@12.23.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + framer-motion: 12.23.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + tslib: 2.8.1 + optionalDependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + ms@2.1.3: {} mz@2.7.0: @@ -6141,65 +6295,65 @@ snapshots: negotiator@1.0.0: {} - next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next-themes@0.4.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@next/env': 15.1.6 + '@next/env': 15.3.5 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 caniuse-lite: 1.0.30001695 postcss: 8.4.31 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - styled-jsx: 5.1.6(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + styled-jsx: 5.1.6(react@19.1.1) optionalDependencies: - '@next/swc-darwin-arm64': 15.1.6 - '@next/swc-darwin-x64': 15.1.6 - '@next/swc-linux-arm64-gnu': 15.1.6 - '@next/swc-linux-arm64-musl': 15.1.6 - '@next/swc-linux-x64-gnu': 15.1.6 - '@next/swc-linux-x64-musl': 15.1.6 - '@next/swc-win32-arm64-msvc': 15.1.6 - '@next/swc-win32-x64-msvc': 15.1.6 - sharp: 0.33.5 + '@next/swc-darwin-arm64': 15.3.5 + '@next/swc-darwin-x64': 15.3.5 + '@next/swc-linux-arm64-gnu': 15.3.5 + '@next/swc-linux-arm64-musl': 15.3.5 + '@next/swc-linux-x64-gnu': 15.3.5 + '@next/swc-linux-x64-musl': 15.3.5 + '@next/swc-win32-arm64-msvc': 15.3.5 + '@next/swc-win32-x64-msvc': 15.3.5 + sharp: 0.34.2 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@4.0.7(@types/react@19.0.8)(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.7(acorn@8.14.0)(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + nextra-theme-docs@4.2.17(@types/react@19.1.9)(next@15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nextra@4.2.17(acorn@8.14.0)(next@15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@headlessui/react': 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@headlessui/react': 2.2.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) clsx: 2.1.1 - next: 15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - next-themes: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - nextra: 4.0.7(acorn@8.14.0)(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) - react: 19.0.0 - react-compiler-runtime: 0.0.0-experimental-22c6e49-20241219(react@19.0.0) - react-dom: 19.0.0(react@19.0.0) + next: 15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next-themes: 0.4.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + nextra: 4.2.17(acorn@8.14.0)(next@15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.3) + react: 19.1.1 + react-compiler-runtime: 0.0.0-experimental-22c6e49-20241219(react@19.1.1) + react-dom: 19.1.1(react@19.1.1) scroll-into-view-if-needed: 3.1.0 zod: 3.24.1 zod-validation-error: 3.4.0(zod@3.24.1) - zustand: 5.0.3(@types/react@19.0.8)(react@19.0.0) + zustand: 5.0.3(@types/react@19.1.9)(react@19.1.1) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - nextra@4.0.7(acorn@8.14.0)(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3): + nextra@4.2.17(acorn@8.14.0)(next@15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.3): dependencies: - '@formatjs/intl-localematcher': 0.5.10 - '@headlessui/react': 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@formatjs/intl-localematcher': 0.6.1 + '@headlessui/react': 2.2.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mdx-js/mdx': 3.1.0(acorn@8.14.0) '@napi-rs/simple-git': 0.1.19 - '@shikijs/twoslash': 1.29.1(typescript@5.7.3) - '@theguild/remark-mermaid': 0.2.0(react@19.0.0) + '@shikijs/twoslash': 2.5.0(typescript@5.7.3) + '@theguild/remark-mermaid': 0.2.0(react@19.1.1) '@theguild/remark-npm2yarn': 0.3.3 - better-react-mathjax: 2.0.3(react@19.0.0) + better-react-mathjax: 2.0.3(react@19.1.1) clsx: 2.1.1 estree-util-to-js: 2.0.0 estree-util-value-to-estree: 3.2.1 @@ -6211,24 +6365,25 @@ snapshots: mdast-util-gfm: 3.0.0 mdast-util-to-hast: 13.2.0 negotiator: 1.0.0 - next: 15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-compiler-runtime: 0.0.0-experimental-22c6e49-20241219(react@19.0.0) - react-dom: 19.0.0(react@19.0.0) - react-medium-image-zoom: 5.2.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.3.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-compiler-runtime: 0.0.0-experimental-22c6e49-20241219(react@19.1.1) + react-dom: 19.1.1(react@19.1.1) + react-medium-image-zoom: 5.2.13(react-dom@19.1.1(react@19.1.1))(react@19.1.1) rehype-katex: 7.0.1 - rehype-pretty-code: 0.14.0(shiki@1.29.1) + rehype-pretty-code: 0.14.1(shiki@2.5.0) rehype-raw: 7.0.0 remark-frontmatter: 5.0.0 remark-gfm: 4.0.0 remark-math: 6.0.0 remark-reading-time: 2.0.1 remark-smartypants: 3.0.2 - shiki: 1.29.1 + shiki: 2.5.0 slash: 5.1.0 title: 4.0.1 unist-util-remove: 4.0.0 unist-util-visit: 5.0.0 + unist-util-visit-children: 3.0.0 yaml: 2.7.0 zod: 3.24.1 zod-validation-error: 3.4.0(zod@3.24.1) @@ -6296,11 +6451,11 @@ snapshots: dependencies: mimic-fn: 4.0.0 - oniguruma-to-es@2.3.0: + oniguruma-to-es@3.1.1: dependencies: emoji-regex-xs: 1.0.0 - regex: 5.1.1 - regex-recursion: 5.1.1 + regex: 6.0.1 + regex-recursion: 6.0.2 optionator@0.9.4: dependencies: @@ -6451,7 +6606,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-tailwindcss@0.6.11(prettier@3.4.2): + prettier-plugin-tailwindcss@0.6.13(prettier@3.4.2): dependencies: prettier: 3.4.2 @@ -6469,23 +6624,23 @@ snapshots: queue-microtask@1.2.3: {} - react-compiler-runtime@0.0.0-experimental-22c6e49-20241219(react@19.0.0): + react-compiler-runtime@0.0.0-experimental-22c6e49-20241219(react@19.1.1): dependencies: - react: 19.0.0 + react: 19.1.1 - react-dom@19.0.0(react@19.0.0): + react-dom@19.1.1(react@19.1.1): dependencies: - react: 19.0.0 - scheduler: 0.25.0 + react: 19.1.1 + scheduler: 0.26.0 react-is@16.13.1: {} - react-medium-image-zoom@5.2.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-medium-image-zoom@5.2.13(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - react@19.0.0: {} + react@19.1.1: {} read-cache@1.0.0: dependencies: @@ -6538,14 +6693,13 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 - regex-recursion@5.1.1: + regex-recursion@6.0.2: dependencies: - regex: 5.1.1 regex-utilities: 2.3.0 regex-utilities@2.3.0: {} - regex@5.1.1: + regex@6.0.1: dependencies: regex-utilities: 2.3.0 @@ -6574,13 +6728,13 @@ snapshots: hast-util-from-html: 2.0.3 unified: 11.0.5 - rehype-pretty-code@0.14.0(shiki@1.29.1): + rehype-pretty-code@0.14.1(shiki@2.5.0): dependencies: '@types/hast': 3.0.4 hast-util-to-string: 3.0.1 parse-numeric-range: 1.3.0 rehype-parse: 9.0.1 - shiki: 1.29.1 + shiki: 2.5.0 unified: 11.0.5 unist-util-visit: 5.0.0 @@ -6754,7 +6908,7 @@ snapshots: safer-buffer@2.1.2: {} - scheduler@0.25.0: {} + scheduler@0.26.0: {} scroll-into-view-if-needed@3.1.0: dependencies: @@ -6764,6 +6918,9 @@ snapshots: semver@7.6.3: {} + semver@7.7.2: + optional: true + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -6786,31 +6943,33 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 - sharp@0.33.5: + sharp@0.34.2: dependencies: color: 4.2.3 - detect-libc: 2.0.3 - semver: 7.6.3 + detect-libc: 2.0.4 + semver: 7.7.2 optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 + '@img/sharp-darwin-arm64': 0.34.2 + '@img/sharp-darwin-x64': 0.34.2 + '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-linux-arm': 0.34.2 + '@img/sharp-linux-arm64': 0.34.2 + '@img/sharp-linux-s390x': 0.34.2 + '@img/sharp-linux-x64': 0.34.2 + '@img/sharp-linuxmusl-arm64': 0.34.2 + '@img/sharp-linuxmusl-x64': 0.34.2 + '@img/sharp-wasm32': 0.34.2 + '@img/sharp-win32-arm64': 0.34.2 + '@img/sharp-win32-ia32': 0.34.2 + '@img/sharp-win32-x64': 0.34.2 optional: true shebang-command@2.0.0: @@ -6819,15 +6978,15 @@ snapshots: shebang-regex@3.0.0: {} - shiki@1.29.1: + shiki@2.5.0: dependencies: - '@shikijs/core': 1.29.1 - '@shikijs/engine-javascript': 1.29.1 - '@shikijs/engine-oniguruma': 1.29.1 - '@shikijs/langs': 1.29.1 - '@shikijs/themes': 1.29.1 - '@shikijs/types': 1.29.1 - '@shikijs/vscode-textmate': 10.0.1 + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 side-channel-list@1.0.0: @@ -6867,6 +7026,11 @@ snapshots: slash@5.1.0: {} + sonner@2.0.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + source-map-js@1.2.1: {} source-map@0.7.4: {} @@ -6968,10 +7132,10 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(react@19.0.0): + styled-jsx@5.1.6(react@19.1.1): dependencies: client-only: 0.0.1 - react: 19.0.0 + react: 19.1.1 stylis@4.3.5: {} @@ -7123,7 +7287,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.19.8: {} + undici-types@6.21.0: {} unified@11.0.5: dependencies: @@ -7316,9 +7480,9 @@ snapshots: zod@3.24.1: {} - zustand@5.0.3(@types/react@19.0.8)(react@19.0.0): + zustand@5.0.3(@types/react@19.1.9)(react@19.1.1): optionalDependencies: - '@types/react': 19.0.8 - react: 19.0.0 + '@types/react': 19.1.9 + react: 19.1.1 zwitch@2.0.4: {} diff --git a/docs/src/app/[lang]/docs/[[...mdxPath]]/page.tsx b/docs/src/app/[lang]/[[...mdxPath]]/page.tsx similarity index 90% rename from docs/src/app/[lang]/docs/[[...mdxPath]]/page.tsx rename to docs/src/app/[lang]/[[...mdxPath]]/page.tsx index 1420818..37e0e99 100644 --- a/docs/src/app/[lang]/docs/[[...mdxPath]]/page.tsx +++ b/docs/src/app/[lang]/[[...mdxPath]]/page.tsx @@ -1,9 +1,9 @@ import { generateStaticParamsFor, importPage } from "nextra/pages"; -import { useMDXComponents } from "../../../../../mdx-components"; +import { useMDXComponents } from "@/mdx-components"; import { TrackPageView } from "@/tracker"; -import { Lang } from "@/lib/types/lang"; +import type { Lang } from "@/lib/types/lang"; export const generateStaticParams = generateStaticParamsFor("mdxPath"); diff --git a/docs/src/app/[lang]/_meta.ts b/docs/src/app/[lang]/_meta.ts deleted file mode 100644 index c50c667..0000000 --- a/docs/src/app/[lang]/_meta.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MetaRecord } from "nextra"; - -const meta: MetaRecord = { - index: { - title: "Home", - type: "page", - display: "hidden", - }, - docs: { - title: "Documentation", - type: "page", - }, -}; - -export default meta; diff --git a/docs/src/app/[lang]/layout.tsx b/docs/src/app/[lang]/layout.tsx index a3f739e..2e5efa7 100644 --- a/docs/src/app/[lang]/layout.tsx +++ b/docs/src/app/[lang]/layout.tsx @@ -7,19 +7,20 @@ import "nextra-theme-docs/style-prefixed.css"; import "../globals.css"; import { TrackClick, TrackImpression, TrackProvider } from "@/tracker"; import { Logo } from "@/logo"; -import { Lang } from "@/lib/types/lang"; -import { type Metadata } from "next"; +import type { Lang } from "@/lib/types/lang"; +import type { Metadata } from "next"; export const metadata: Metadata = { title: { template: "%s | Event Tracker", - default: "Event Tracker", + default: "Event Tracker | Comprehensive solution for event tracking in React applications", }, description: "Comprehensive solution for event tracking in React applications.", + metadataBase: new URL("https://event-tracker.offlegacy.org"), openGraph: { title: { template: "%s | Event Tracker", - default: "Event Tracker", + default: "Event Tracker | Comprehensive solution for event tracking in React applications", }, images: ["img/og-webp.webp"], description: "Comprehensive solution for event tracking in React applications.", @@ -35,7 +36,7 @@ const navbar = (
- Event Tracker + Event Tracker
} @@ -46,7 +47,7 @@ const navbar = ( ); const footer = ( - + ); @@ -81,7 +82,7 @@ export default async function RootLayout({ pageMap={pageMap} docsRepositoryBase="https://github.com/offlegacy/event-tracker/tree/main/docs" footer={footer} - darkMode + darkMode={false} > {children} diff --git a/docs/src/app/[lang]/page.tsx b/docs/src/app/[lang]/page.tsx deleted file mode 100644 index 61cc581..0000000 --- a/docs/src/app/[lang]/page.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import Link from "next/link"; -import { Image } from "nextra/components"; - -import { Logo } from "@/logo"; -import { TrackClick, TrackPageView } from "@/tracker"; -import { importPage } from "nextra/pages"; -import { Lang } from "@/lib/types/lang"; - -export const metadata = {}; - -const TEXT_MAP = { - description: { - ko: "React 애플리케이션을 위한 완전한 이벤트 트래킹 시스템.", - en: "Comprehensive solution for event tracking in React applications.", - }, - get_started: { - ko: "시작하기", - en: "Get Started", - }, - featureCards: [ - { - ko: { - title: "타입 안전성이 보장된 API", - description: "타입 안전성을 갖춘 선언적 이벤트 트래킹 API를 제공합니다.", - }, - en: { - title: "Type-safe APIs", - description: "Declarative event tracking with complete type safety", - }, - }, - { - ko: { - title: "최적화된 성능", - description: "이벤트 배칭을 통한 향상된 성능을 제공합니다.", - }, - en: { - title: "Optimized Performance", - description: "Enhanced performance through event batching", - }, - }, - { - ko: { - title: "순서 보장", - description: "비동기 작업의 실행 순서가 보장됩니다.", - }, - en: { - title: "Guaranteed Order", - description: "Guaranteed execution order for async operations", - }, - }, - { - ko: { - title: "애널리틱스 도구에 구애받지 않음", - description: "모든 애널리틱스 도구와 호환 가능합니다.", - }, - en: { - title: "Analytics Integration", - description: "Works with any analytics provider of your choice", - }, - }, - { - ko: { - title: "관심사의 분리", - description: "비즈니스 로직과 이벤트 트래킹 로직의 명확한 분리를 제공합니다.", - }, - en: { - title: "Clean Separation", - description: "Keeps tracking logic separate from business logic", - }, - }, - { - ko: { - title: "작은 번들 사이즈", - description: "애플리케이션 번들 크기에 최소한의 영향을 미칩니다.", - }, - en: { - title: "Lightweight", - description: "Minimal bundle size impact on your application", - }, - }, - ], -} as const; - -export default async function Page(props: { params: Promise<{ mdxPath: string[]; lang: Lang }> }) { - const { mdxPath, lang } = await props.params; - await importPage(mdxPath, lang); - - return ( - <> -
-
- -

event-tracker

-

{TEXT_MAP.description[lang]}

-
- - MIT License - - - NPM Version - -
- - - {TEXT_MAP.get_started[lang]} - - -
- -
- - - - - - -
-
- - - ); -} - -function FeatureCard({ icon, title, description }: { icon: string; title: string; description: string }) { - return ( -
-
{icon}
-

{title}

-

{description}

-
- ); -} diff --git a/docs/src/components/demo-button.tsx b/docs/src/components/demo-button.tsx new file mode 100644 index 0000000..b2c4a70 --- /dev/null +++ b/docs/src/components/demo-button.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { createTracker } from "@offlegacy/event-tracker"; +import { Toaster, toast } from "sonner"; +import { motion } from "motion/react"; + +const [Track] = createTracker({ + DOMEvents: { + onClick: (params: { buttonId: string }, context: { userId: string }) => { + toast.success(`${context.userId}: ${params.buttonId}`); + }, + }, +}); + +export function DemoButton({ children }: { children: React.ReactNode }) { + return ( + <> + + + + + {children} + + + + + ); +} diff --git a/docs/src/components/demo-playground.tsx b/docs/src/components/demo-playground.tsx new file mode 100644 index 0000000..1d3e86e --- /dev/null +++ b/docs/src/components/demo-playground.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { highlight, Pre, type HighlightedCode } from "codehike/code"; +import { DemoButton } from "./demo-button"; +import { useTheme } from "nextra-theme-docs"; +import { useSystemDarkMode } from "../logo/useSystemDarkMode"; +import { useEffect, useState } from "react"; +import { motion } from "motion/react"; + +const code = `import { createTracker } from "@offlegacy/event-tracker"; +import { capture } from "you-can-use-analytics"; + +const [Track] = createTracker({ + DOMEvents: { + onClick: (params, context) => { + capture("click", { ...params, ...context }); + toast.success(\`\${context.userId}: \${params.buttonId}\`); + }, + }, +}); + +function App() { + return ( + + + + + + ); +}`; + +export function DemoCode() { + const { theme } = useTheme(); + const isSystemDarkMode = useSystemDarkMode(); + const resolvedTheme = theme === "system" ? (isSystemDarkMode ? "dark" : "light") : theme; + const codeTheme = resolvedTheme === "dark" ? "github-dark" : "github-light"; + const [highlighted, setHighlighted] = useState(null); + + useEffect(() => { + highlight( + { + value: code, + lang: "tsx", + meta: "", + }, + codeTheme, + ).then(setHighlighted); + }, [codeTheme]); + + if (!highlighted) return null; + + return ( + +
+
+
+ example-basic.tsx +
+
+
+
+        
+
+
+ Click me +
+
+ ); +} diff --git a/docs/src/components/home-page.tsx b/docs/src/components/home-page.tsx new file mode 100644 index 0000000..4dbb6a0 --- /dev/null +++ b/docs/src/components/home-page.tsx @@ -0,0 +1,89 @@ +import Link from "next/link"; + +import { Logo } from "@/logo"; +import { TrackClick, TrackPageView } from "@/tracker"; +import { DemoCode } from "@/components/demo-playground"; +import Image from "next/image"; +import { InstallCopyButton } from "@/components/install-copy-button"; + +type FeatureCardText = { + title: string; + description: string; +}; + +type Props = { + description: string; + startedText: string; + featureSectionTitle: string; + featureSectionContents: FeatureCardText[]; +}; + +export default async function HomePage({ + description, + startedText, + featureSectionTitle, + featureSectionContents, +}: Props) { + return ( +
+ +
+
+ +
+

Event Tracker

+

{description}

+
+
+ + + {startedText} + + + +
+
+
+ +
+
+
+

{featureSectionTitle}

+
+ {featureSectionContents.map((card) => ( + + ))} +
+
+
+

Driven by the Community

+ + Contributors + +
+
+ ); +} + +function FeatureCard({ title, description }: { title: string; description: string }) { + return ( +
+

{title}

+

{description}

+
+ ); +} diff --git a/docs/src/components/install-copy-button.tsx b/docs/src/components/install-copy-button.tsx new file mode 100644 index 0000000..cd16267 --- /dev/null +++ b/docs/src/components/install-copy-button.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { Copy, Check } from "lucide-react"; +import { motion, AnimatePresence } from "motion/react"; +import { useCopyToClipboard } from "@uidotdev/usehooks"; +import { useState } from "react"; + +const COMMAND = "npm i @offlegacy/event-tracker"; + +export function InstallCopyButton() { + const [, copyToClipboard] = useCopyToClipboard(); + const [showCheck, setShowCheck] = useState(false); + + return ( + + ); +} diff --git a/docs/src/content/en/_meta.tsx b/docs/src/content/en/_meta.tsx new file mode 100644 index 0000000..43b6655 --- /dev/null +++ b/docs/src/content/en/_meta.tsx @@ -0,0 +1,16 @@ +import type { MetaRecord } from "nextra"; + +export default { + index: { + type: "page", + display: "hidden", + theme: { + layout: "full", + toc: false, + }, + }, + docs: { + type: "page", + title: "Docs", + }, +} satisfies MetaRecord; diff --git a/docs/src/content/en/batching.mdx b/docs/src/content/en/batching.mdx deleted file mode 100644 index 9b4be09..0000000 --- a/docs/src/content/en/batching.mdx +++ /dev/null @@ -1,128 +0,0 @@ -# Event Batching - -## Overview - -Event batching is a performance optimization technique that groups multiple events together before sending them to your analytics service. This is particularly useful when: - -- You're not using an analytics service with built-in batching (like Amplitude or Google Analytics) -- You need to reduce the number of network requests -- You want to optimize for performance at scale - -## Configuration - -Enable batching in your tracker configuration: - -```tsx -const [Track, useTracker] = createTracker({ - // ... other config - batch: { - enable: true, - interval: 2000, - thresholdSize: 100, - onFlush: (batch, isBrowserClosing) => { - if (isBrowserClosing) { - // Use sendBeacon for reliable delivery when browser is closing - navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); - return; - } - - // Normal batch submission - fetch("/analytics/collect/batch", { - method: "POST", - body: JSON.stringify(batch), - }); - }, - }, -}); -``` - -### Options - -- `enable: boolean` (default: `false`) - - Enables/disables batching -- `interval: number` (default: `3000`) - - Time in milliseconds between batch flushes -- `thresholdSize: number` (default: `25`) - - Maximum number of events in a batch before forcing a flush -- `onFlush: (batch: Batch[], isBrowserClosing: boolean) => void | Promise` - - Required when batching is enabled - - Function called to process the batch - - `isBrowserClosing` indicates if the flush was triggered by browser closure -- `onError: (error: Error) => void | Promise` - - Optional error handler for batch processing failures - -## How It Works - -1. Events are collected in memory -2. The batch is flushed when either: - - The configured interval is reached - - The batch size reaches the threshold - - The browser is about to close/hide - -### Example Flow - -```tsx -const [Track, useTracker] = createTracker({ - DOMEvents: { - onClick: (clickParams, context) => { - // Return value is what gets batched - return { - ...clickParams, - ...context, - event_type: "click", - timestamp: new Date().getTime(), - }; - }, - }, - batch: { - enable: true, - interval: 2000, - thresholdSize: 100, - onFlush: (batch, isBrowserClosing) => { - // Process the batch - }, - }, -}); - -function App() { - return ( - - - - - - ); -} -``` - -When the button is clicked multiple times: - -1. Each click runs the `onClick` handler -2. The return value from each handler is added to the batch queue -3. After 2 seconds (or when threshold is reached), `onFlush` is called with all collected events - -## Browser Closing Behavior - -When the browser is closing, you should use [`navigator.sendBeacon`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon) to ensure reliable delivery: - -```tsx -onFlush: (batch, isBrowserClosing) => { - if (isBrowserClosing) { - navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); - return; - } - - // Normal operation - fetch("/analytics/collect/batch", { - method: "POST", - body: JSON.stringify(batch), - }); -}; -``` - -## Important Notes - -1. Only the **return values** from event handlers are batched, not the handlers themselves -2. Each event in the batch maintains its original parameters and context -3. Events are processed in order, maintaining the sequence of user actions -4. Use `navigator.sendBeacon` for browser closing scenarios to ensure delivery diff --git a/docs/src/content/en/components.mdx b/docs/src/content/en/components.mdx deleted file mode 100644 index 64113ec..0000000 --- a/docs/src/content/en/components.mdx +++ /dev/null @@ -1,189 +0,0 @@ -# Components - -event-tracker provides several components for tracking different types of events. Each component is designed to be easy to use while maintaining type safety and performance. - -## Provider - -The `Provider` component connects and provides initial context to your application. - -```tsx -import { createTracker } from '@offlegacy/event-tracker' - -const [Track] = createTracker({...}) - -function App() { - return ( - - {/* Your app content */} - - ) -} -``` - -### Props - -- `initialContext: unknown` - Initial context value - -## DOMEvent - -Used for tracking DOM events. Wraps a child component and fires the specified event handler. - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -const [Track] = createTracker({ - DOMEvents: { - onFocus: (params, context) => { - // Handle focus event - }, - }, -}); - -function App() { - return ( - - - - - - ); -} -``` - -### Props - -- `type: DOMEventNames` - Event name (e.g., onClick, onFocus) -- (With schema) - - `params: SchemaParams | (context: Context) => SchemaParams` - Event parameters - - `schema?: string` - A name of schema that will be used to validate the event parameters -- (Without schema) - - `params: EventParams | (context: Context) => EventParams` - Event parameters - -## Click - -A specialized version of `DOMEvent` for click events (`type="onClick"`). - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -const [Track] = createTracker({ - DOMEvents: { - onClick: (params, context) => { - // Handle click event - }, - }, -}); - -function App() { - return ( - - - - - - ); -} -``` - -### Props - -- With schema - - `params: SchemaParams | (context: Context) => SchemaParams` - Click event parameters - - `schema?: string` - A name of schema that will be used to validate the event parameters -- Without schema - - `params: EventParams | (context: Context) => EventParams` - Click event parameters - -## Impression - -Tracks impression events using the Intersection Observer API. - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -const [Track] = createTracker({ - impression: { - onImpression: (params, context) => { - // Handle impression event - }, - options: { - threshold: 0.5, - }, - }, -}); - -function App() { - return ( - - -
Tracked content
-
-
- ); -} -``` - -### Props - -- Without schema - - `params: EventParams | (context: Context) => EventParams` - Impression event parameters - - `options?: ImpressionOptions` - Optional configuration (overrides global options) -- With schema - - `params: SchemaParams | (context: Context) => SchemaParams` - Impression event parameters - - `schema?: string` - A name of schema that will be used to validate the event parameters - - `options?: ImpressionOptions` - Optional configuration (overrides global options) - -## PageView - -Tracks page view events on component mount. - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -const [Track] = createTracker({ - pageView: { - onPageView: (params, context) => { - // Handle page view event - }, - }, -}); - -function App() { - return ( - - - - ); -} -``` - -### Props - -- With schema - - `params: SchemaParams | (context: Context) => SchemaParams` - Page view event parameters - - `schema?: string` - A name of schema that will be used to validate the event parameters -- Without schema - - `params: EventParams | (context: Context) => EventParams` - Page view event parameters - -## SetContext - -Sets or updates the tracking context. - -```tsx -import { createTracker } from '@offlegacy/event-tracker' - -const [Track] = createTracker({...}) - -function App() { - return ( - - - - ) -} -``` - -### Props - -- `context: unknown | ((prevContext: unknown) => unknown)` - New context value or update function diff --git a/docs/src/content/en/components/click.mdx b/docs/src/content/en/components/click.mdx deleted file mode 100644 index 742274f..0000000 --- a/docs/src/content/en/components/click.mdx +++ /dev/null @@ -1,85 +0,0 @@ -# Click - -A specialized version of `DOMEvent` for click events (`type="onClick"`). - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -const [Track] = createTracker({ - DOMEvents: { - onClick: (params, context) => { - // Handle click event - }, - }, -}); - -function App() { - return ( - - - - - - ); -} -``` - -### Props - -- With schema - - `params: SchemaParams | (context: Context) => SchemaParams` - Click event parameters based on schema - - `schema: string` - A name of schema that will be used to validate the event parameters -- Without schema - - `params: EventParams | (context: Context) => EventParams` - Click event parameters -- `enabled?: boolean | ((context: Context, params: EventParams) => boolean)` - Conditionally enable/disable event tracking (default: `true`) -- `debounce?: DebounceConfig` - Debounce configuration to prevent rapid successive events -- `throttle?: ThrottleConfig` - Throttle configuration to limit event frequency - -**Note:** `debounce` and `throttle` are mutually exclusive and cannot be used together. - -### Tracking Options Examples - -#### Conditional Tracking - -```tsx - context.user?.isPremium}> - - -``` - -#### Debounced Tracking - -```tsx - - - -``` - -#### Throttled Tracking - -```tsx - - - -``` - -## Handling Renamed Click Event Prop - -The `eventName` prop allows you to specify a custom click event handler name. -It allows you to dynamically specify the new click event handler name, ensuring flexibility even when the handler names change. - -```tsx {8, 10} -function MyButton({ onButtonClick }: { onButtonClick?: () => void }) { - return + + + ); +} +``` + + diff --git a/docs/src/content/en/docs/batching.mdx b/docs/src/content/en/docs/batching.mdx new file mode 100644 index 0000000..4085051 --- /dev/null +++ b/docs/src/content/en/docs/batching.mdx @@ -0,0 +1,128 @@ +import { Callout } from "nextra/components"; + +# Batching + +Event batching allows you to group multiple events into a single network request under certain conditions. It is particularly useful in the following cases: + +- When using analytics services like Amplitude or Google Analytics that lack built-in batching +- When you want to reduce the number of network requests +- When you need to prevent event loss on browser close + +## Usage + +Enable the `batch` option in the `createTracker` configuration to use batching. + +```tsx +const [Track, useTracker] = createTracker({ + // ... other configuration + batch: { + enable: true, + interval: 2000, + thresholdSize: 100, + onFlush: (batch, isBrowserClosing) => { + if (isBrowserClosing) { + navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); + return; + } + + fetch("/analytics/collect/batch", { + method: "POST", + body: JSON.stringify(batch), + }); + }, + }, +}); +``` + +## Options + +| Option | Type | Description | Default | +| --------------- | ---------------------------------------------------- | ------------------------------------------------- | ------- | +| `enable` | `boolean` | Whether to enable batching | `false` | +| `interval` | `number` | Interval between batch sends (in ms) | `3000` | +| `thresholdSize` | `number` | Maximum number of events before flushing batch | `25` | +| `onFlush` | `(batch, isBrowserClosing) => void \| Promise` | Required. Function to process batched events | - | +| `onError` | `(error: Error) => void \| Promise` | Optional. Called when an error occurs during send | - | + +`onFlush` is **required** and defines how batched events are actually sent. + +## How It Works + +1. The **return value** of each event handler is collected in memory. +2. `onFlush` is called when one of the following occurs: + + - The `interval` time has elapsed + - The number of collected events reaches `thresholdSize` + - A browser close event is detected + +## Examples + +```tsx +const [Track, useTracker] = createTracker({ + DOMEvents: { + onClick: (params, context) => { + return { + ...params, + ...context, + event_type: "click", + timestamp: Date.now(), + }; + }, + }, + batch: { + enable: true, + interval: 2000, + thresholdSize: 100, + onFlush: (batch, isBrowserClosing) => { + if (isBrowserClosing) { + navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); + return; + } + + fetch("/analytics/collect/batch", { + method: "POST", + body: JSON.stringify(batch), + }); + }, + }, +}); + +function App() { + return ( + + + + + + ); +} +``` + +1. Each time `` runs, the object returned by the event handler is added to the batch queue. +2. After 2 seconds or once 100 events accumulate, `onFlush` is called and the batch is sent to the server. +3. On browser close, events are sent via `sendBeacon`. + +## Handling Browser Close + +To prevent event loss when the browser closes, you can use [`navigator.sendBeacon`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon): + +```tsx +onFlush: (batch, isBrowserClosing) => { + if (isBrowserClosing) { + navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); + return; + } + + fetch("/analytics/collect/batch", { + method: "POST", + body: JSON.stringify(batch), + }); +}; +``` + +## Notes + +1. **Only the return value of event handlers is batched.** The handlers themselves are always executed. +2. **Batched events are stored with context information.** +3. **Event order is preserved.** For example, click → impression events are sent in the same order. +4. When the browser closes, the `isBrowserClosing` flag is set to `true` to allow conditional logic in `onFlush`. diff --git a/docs/src/content/en/components/_meta.ts b/docs/src/content/en/docs/components/_meta.ts similarity index 100% rename from docs/src/content/en/components/_meta.ts rename to docs/src/content/en/docs/components/_meta.ts diff --git a/docs/src/content/en/docs/components/click.mdx b/docs/src/content/en/docs/components/click.mdx new file mode 100644 index 0000000..4207280 --- /dev/null +++ b/docs/src/content/en/docs/components/click.mdx @@ -0,0 +1,154 @@ +import { Callout } from "nextra/components"; + +# Click + +A dedicated `DOMEvent` component for click events (`type="onClick"`). +It is one of the event components returned as the first element of the `createTracker` tuple. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ + DOMEvents: { + onClick: (params, context) => { + // Handle click event + }, + }, +}); + +function App() { + return ( + + + + + + ); +} +``` + +## Signature + +```ts +function Click({ + children, + enabled, + ...props +}: { + children: ReactNode; + enabled?: EnabledCondition; +} & TrackingOptions & + UnionPropsWithAndWithoutSchema): JSX.Element; +``` + +## Reference + +### Props + +| Prop | Type | Description | Required | +| ---------- | ----------------------------------------------------------------- | ---------------------------------------------------------- | -------- | +| `enabled` | `boolean \| ((context: Context, params: EventParams) => boolean)` | Conditionally enable/disable tracking (default: `true`) | - | +| `debounce` | `DebounceConfig` | Debounce configuration to prevent rapid consecutive clicks | - | +| `throttle` | `ThrottleConfig` | Throttle configuration to limit click frequency | - | +| `schema` | `string` | Schema name for validating event parameters | O | + + + Note: `debounce` and `throttle` are mutually exclusive and cannot be used together. A type error prevents invalid + usage. + + +The type of the `params` prop differs depending on whether a schema is used. + +With schema: + +| Prop | Type | Description | +| -------- | ---------------------------------------------------- | ----------------------- | +| `params` | `SchemaParams \| (context: Context) => SchemaParams` | Schema-based parameters | + +Without schema: + +| Prop | Type | Description | +| -------- | -------------------------------------------------- | ---------------- | +| `params` | `EventParams \| (context: Context) => EventParams` | Event parameters | + +--- + +#### `params` (Required) + +With schema: + +- **Type:** `SchemaParams | (context: Context) => SchemaParams` +- **Description:** Schema-based parameters. + +Without schema: + +- **Type:** `EventParams | (context: Context) => EventParams` +- **Description:** Standard event parameters. + +#### `enabled` + +- **Type:** `boolean | ((context: Context, params: EventParams) => boolean)` +- **Description:** Conditionally enable or disable event tracking (default: `true`). + +#### `debounce` + +- **Type:** `DebounceConfig` +- **Description:** Configure debouncing to avoid multiple rapid click triggers. + +#### `throttle` + +- **Type:** `ThrottleConfig` +- **Description:** Configure throttling to limit click frequency. + +#### `schema` + +When using schemas: + +- **Type:** `string` +- **Description:** Schema name used to validate event parameters. + +## Examples + +### Custom Event Handler Name + +```tsx {8, 10} +function MyButton({ onButtonClick }: { onButtonClick?: () => void }) { + return + +``` + +### Debounced Event + +```tsx + + + +``` + +### Throttled Event + +```tsx + + + +``` diff --git a/docs/src/content/en/docs/components/dom-event.mdx b/docs/src/content/en/docs/components/dom-event.mdx new file mode 100644 index 0000000..b3fdf48 --- /dev/null +++ b/docs/src/content/en/docs/components/dom-event.mdx @@ -0,0 +1,149 @@ +import { Callout } from "nextra/components"; + +# DOMEvent + +Used to track DOM events. Wraps a child component and triggers the specified event handler. +It is one of the event components returned as the first element of the `createTracker` tuple. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ + DOMEvents: { + onFocus: (params, context) => { + // Handle focus event + }, + }, +}); + +function App() { + return ( + + + + + + ); +} +``` + +## Reference + +### Props + +| Prop | Type | Description | Required | +| ---------- | ----------------------------------------------------------------- | ---------------------------------------------------------- | -------- | +| `type` | `DOMEventNames` | Event name (e.g., `"onClick"`, `"onFocus"`) | Yes | +| `enabled` | `boolean \| ((context: Context, params: EventParams) => boolean)` | Conditionally enable/disable tracking (default: `true`) | - | +| `debounce` | `DebounceConfig` | Debounce configuration to prevent rapid consecutive events | - | +| `throttle` | `ThrottleConfig` | Throttle configuration to limit event frequency | - | +| `schema` | `string` | Schema name for validating event parameters | - | + + + Note: `debounce` and `throttle` are mutually exclusive and cannot be used together. A type error prevents invalid + usage. + + +The type of the `params` prop differs depending on whether a schema is used. + +With schema: + +| Prop | Type | Description | +| -------- | ---------------------------------------------------- | ----------------------- | +| `params` | `SchemaParams \| (context: Context) => SchemaParams` | Schema-based parameters | + +Without schema: + +| Prop | Type | Description | +| -------- | -------------------------------------------------- | ---------------- | +| `params` | `EventParams \| (context: Context) => EventParams` | Event parameters | + +--- + +#### `params` (Required) + +With schema: + +- **Type:** `SchemaParams | (context: Context) => SchemaParams` +- **Description:** Schema-based parameters. + +Without schema: + +- **Type:** `EventParams | (context: Context) => EventParams` +- **Description:** Standard event parameters. + +#### `enabled` + +- **Type:** `boolean | ((context: Context, params: EventParams) => boolean)` +- **Description:** Conditionally enable or disable event tracking (default: `true`). + +#### `debounce` + +- **Type:** `DebounceConfig` +- **Description:** Configure debouncing to avoid multiple rapid event triggers. + +#### `throttle` + +- **Type:** `ThrottleConfig` +- **Description:** Configure throttling to limit event frequency. + +#### `schema` + +When using schemas: + +- **Type:** `string` +- **Description:** Schema name used to validate event parameters. + +## Examples + +### Custom Event Handler Name + +```tsx {8, 10} +function MyInput({ onInputFocus }: { onInputFocus?: () => void }) { + return ; +} + +function App() { + return ( + + + + + + ); +} +``` + +Using the `eventName` prop lets you specify a custom handler name, providing flexibility if the handler name differs from the DOM event name. + +### Conditional Event + +```tsx + context.trackingEnabled}> + + +``` + +### Debounced Event + +```tsx + + + +``` + +### Throttled Event + +```tsx + +
Scrollable Content
+
+``` diff --git a/docs/src/content/en/docs/components/impression.mdx b/docs/src/content/en/docs/components/impression.mdx new file mode 100644 index 0000000..cb993e6 --- /dev/null +++ b/docs/src/content/en/docs/components/impression.mdx @@ -0,0 +1,129 @@ +import { Callout } from "nextra/components"; + +# Impression + +Tracks impression events using the [Intersection Observer API](https://developer.mozilla.org/docs/Web/API/Intersection_Observer_API). +It is one of the event components returned as the first element of the `createTracker` tuple. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ + impression: { + onImpression: (params, context) => { + // Handle impression event + }, + options: { + threshold: 0.5, + }, + }, +}); + +function App() { + return ( + + +
Tracked Content
+
+
+ ); +} +``` + +## Reference + +### Props + +| Prop | Type | Description | Required | +| ---------- | ----------------------------------------------------------------- | ------------------------------------------------------------- | -------- | +| `enabled` | `boolean \| ((context: Context, params: EventParams) => boolean)` | Conditionally enable/disable event tracking (default: `true`) | - | +| `debounce` | `DebounceConfig` | Configure debouncing to prevent rapid consecutive events | - | +| `throttle` | `ThrottleConfig` | Configure throttling to limit event frequency | - | +| `options` | `ImpressionOptions` | Overrides global `IntersectionObserver` options | - | +| `schema` | `string` | Schema name for validating event parameters | O | + + + Note: `debounce` and `throttle` are mutually exclusive and cannot be used together. A type error will prevent invalid + usage. + + +The `params` prop type changes depending on whether schemas are used. + +With schema: + +| Prop | Type | Description | +| -------- | ---------------------------------------------------- | ----------------------- | +| `params` | `SchemaParams \| (context: Context) => SchemaParams` | Schema-based parameters | + +Without schema: + +| Prop | Type | Description | +| -------- | -------------------------------------------------- | ------------------- | +| `params` | `EventParams \| (context: Context) => EventParams` | Standard parameters | + +--- + +#### `params` (Required) + +With schema: + +- **Type:** `SchemaParams | (context: Context) => SchemaParams` +- **Description:** Schema-based parameters + +Without schema: + +- **Type:** `EventParams | (context: Context) => EventParams` +- **Description:** Event parameters + +#### `enabled` + +- **Type:** `boolean | ((context: Context, params: EventParams) => boolean)` +- **Description:** Conditionally enable or disable event tracking (default: `true`) + +#### `debounce` + +- **Type:** `DebounceConfig` +- **Description:** Configure debouncing to avoid multiple rapid event triggers. + +#### `throttle` + +- **Type:** `ThrottleConfig` +- **Description:** Configure throttling to limit event frequency. + +#### `options` + +- **Type:** `ImpressionOptions` +- **Description:** Controls [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) behavior such as `threshold` and `rootMargin`. Overrides global options for fine-grained control per impression. + +#### `schema` + +When using schemas: + +- **Type:** `string` +- **Description:** Schema name used to validate event parameters. + +## Examples + +### Conditional Event + +```tsx + context.user?.hasAccess} + options={{ threshold: 0.5 }} +> +
Premium Content
+
+``` + +### Throttled Event + +```tsx + +
Ad Banner
+
+``` diff --git a/docs/src/content/en/docs/components/index.mdx b/docs/src/content/en/docs/components/index.mdx new file mode 100644 index 0000000..d96617b --- /dev/null +++ b/docs/src/content/en/docs/components/index.mdx @@ -0,0 +1,63 @@ +# Provider + +The `Provider` component initializes and supplies context to the application. It is one of the event components returned as the first element of the `createTracker` tuple. + +```tsx +import { createTracker } from '@offlegacy/event-tracker' + +const [Track] = createTracker({...}) + +function App() { + return ( + + {/* Application */} + + ) +} +``` + +## Reference + +### Props + +| Prop | Type | Description | Required | +| ---------------- | --------- | --------------------- | -------- | +| `initialContext` | `Context` | Initial context value | Yes | + +--- + +#### `initialContext` (Required) + +- **Type:** `Context` +- **Description:** Defines the initial context value. + +## Examples + +### With Initial Context + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +type Context = { + userId: string; + trackingEnabled: boolean; +}; + +const [Track] = createTracker({}); + +function App() { + return ( + + + + ); +} +``` + +`initialContext` defines the initial state accessible to all nested tracking components. +This context can be used within `enabled`, `params`, and `SetContext`. diff --git a/docs/src/content/en/docs/components/page-view.mdx b/docs/src/content/en/docs/components/page-view.mdx new file mode 100644 index 0000000..d78fd8a --- /dev/null +++ b/docs/src/content/en/docs/components/page-view.mdx @@ -0,0 +1,108 @@ +import { Callout } from "nextra/components"; + +# PageView + +Tracks a page view event when the component mounts. It is one of the event components returned as the first element of the `createTracker` tuple. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ + pageView: { + onPageView: (params, context) => { + // Handle page view event + }, + }, +}); + +function App() { + return ( + + + + ); +} +``` + +## Reference + +### Props + +| Prop | Type | Description | Required | +| ---------- | ----------------------------------------------------------------- | ------------------------------------------------------------- | -------- | +| `enabled` | `boolean \| ((context: Context, params: EventParams) => boolean)` | Conditionally enable/disable event tracking (default: `true`) | - | +| `debounce` | `DebounceConfig` | Debounce configuration to prevent rapid consecutive events | - | +| `throttle` | `ThrottleConfig` | Throttle configuration to limit event frequency | - | +| `schema` | `string` | Schema name for validating event parameters | O | + + + Note: `debounce` and `throttle` are mutually exclusive and cannot be used together. A type error will prevent invalid + usage. + + +When using with schemas vs. without schemas, the `params` prop type differs. + +With schema: + +| Prop | Type | Description | +| -------- | ---------------------------------------------------- | ----------------------- | +| `params` | `SchemaParams \| (context: Context) => SchemaParams` | Schema-based parameters | + +Without schema: + +| Prop | Type | Description | +| -------- | -------------------------------------------------- | ------------------- | +| `params` | `EventParams \| (context: Context) => EventParams` | Standard parameters | + +--- + +#### `params` (Required) + +With schema: + +- **Type:** `SchemaParams | (context: Context) => SchemaParams` +- **Description:** Schema-based parameters + +Without schema: + +- **Type:** `EventParams | (context: Context) => EventParams` +- **Description:** Event parameters + +#### `enabled` + +- **Type:** `boolean | ((context: Context, params: EventParams) => boolean)` +- **Description:** Conditionally enable or disable event tracking (default: `true`) + +#### `debounce` + +- **Type:** `DebounceConfig` +- **Description:** Configure debouncing to avoid multiple rapid event triggers. + +#### `throttle` + +- **Type:** `ThrottleConfig` +- **Description:** Configure throttling to limit event frequency. + +#### `schema` + +When using schemas: + +- **Type:** `string` +- **Description:** Schema name used to validate event parameters. + +## Examples + +### Conditional Event + +```tsx + context.trackingConsent === true} +/> +``` + +### Debounced Page View + +```tsx + +``` diff --git a/docs/src/content/en/docs/components/set-context.mdx b/docs/src/content/en/docs/components/set-context.mdx new file mode 100644 index 0000000..fc4cbf8 --- /dev/null +++ b/docs/src/content/en/docs/components/set-context.mdx @@ -0,0 +1,48 @@ +import { Callout } from "nextra/components"; + +# SetContext + +Sets or updates the tracking context. It is one of the utility components returned as the first element of the `createTracker` tuple. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ ... }); + +function App() { + return ( + + + + ); +} +``` + +## Reference + +### Props + +| Prop | Type | Description | Required | +| --------- | ------------------------------------------------ | ----------------------------------------------------------------------- | -------- | +| `context` | `Context \| ((prevContext: Context) => Context)` | New context value or a function to update based on the previous context | Yes | + +--- + +#### `context` (Required) + +- **Type:** `Context | ((prevContext: Context) => Context)` +- **Description:** You can either specify a new context directly or update it based on the previous context. Once rendered, this component updates the tracking context with the specified value. + +## Examples + +### Set Static Context + +```tsx + +``` + +### Update Based on Previous Context + +```tsx + ({ ...prev, lastSeen: new Date() })} /> +``` diff --git a/docs/src/content/en/docs/create-tracker.mdx b/docs/src/content/en/docs/create-tracker.mdx new file mode 100644 index 0000000..abb506c --- /dev/null +++ b/docs/src/content/en/docs/create-tracker.mdx @@ -0,0 +1,128 @@ +# createTracker + +`createTracker` is a function that generates a tracker instance with custom configurations. +Through its parameters, you define event tracking behavior, and the returned tuple provides access to the tracker instance. + +```tsx +const [{ Provider, DOMEvent, Click, Impression, PageView, SetContext }, useTracker] = createTracker({ + init, + send, + DOMEvents, + impression: { + onImpression, + options, + }, + pageView: { + onPageView, + }, + batch, + schemas, +}); +``` + +## Reference + +### Parameters + +`createTracker` accepts a single configuration object. + +| Property | Type | Description | +| ------------ | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | +| `init` | `(initialContext: Context, setContext: SetContext) => void \| Promise` | Initialization function executed before any event occurs. Async supported. | +| `send` | `(params: EventParams \| (context: Context) => EventParams, context: Context, setContext: SetContext) => TaskReturnType` | Event dispatch function. Async supported. | +| `DOMEvents` | [`DOMEvents`](https://developer.mozilla.org/docs/Web/API/Event) | Collection of React DOM event handlers (`onClick`, `onMouseEnter`, etc.) | +| `impression` | `ImpressionOptions` | Impression event tracking configuration | +| `pageView` | `PageViewOptions` | Page view tracking configuration | +| `batch` | `BatchConfig` | Event batching configuration | +| `schemas` | `SchemaConfig` | Event schema validation configuration | + +--- + +#### `init` + +- **Type:** `(initialContext: Context, setContext: SetContext) => void | Promise` +- **Description:** Function to initialize context before any event fires. Supports async. + +#### `send` + +- **Type:** `(params: EventParams | (context: Context) => EventParams, context: Context, setContext: SetContext) => TaskReturnType` +- **Description:** Sends events to an external service. Supports async. + +#### `DOMEvents` + +- **Type:** `DOMEvents` +- **Description:** Standard React DOM event handler object. Used in `` components. + +#### `impression` + +- **`impression.onImpression`** + + - **Type:** `(params: EventParams | (context: Context) => EventParams, context: Context, setContext: SetContext) => TaskReturnType` + - **Description:** Callback executed when `` fires. + +- **`impression.options`** + + - **Type:** `ImpressionOptions` + - **Properties:** + + - `threshold`: Intersection ratio threshold (default: `0.2`) + - `freezeOnceVisible`: Whether to lock state after first impression (default: `true`) + - `initialIsIntersecting`: Initial intersection state (default: `false`) + +#### `pageView` + +- **`pageView.onPageView`** + + - **Type:** `(params: EventParams | (context: Context) => EventParams, context: Context, setContext: SetContext) => TaskReturnType` + - **Description:** Callback executed when `` mounts. + +#### `batch` + +- **Type:** `BatchConfig` +- **Properties:** + + - `batch.enable`: Enable batching (default: `false`) + - `batch.interval`: Dispatch interval in ms (default: `3000`) + - `batch.thresholdSize`: Max batch size (default: `25`) + - `batch.onFlush`: Function to process batched events + - `batch.onError`: Optional error handler for batch failures + +#### `schemas` + +- **Type:** `SchemaConfig` +- **Properties:** + + - `schemas.schema`: [Zod](https://zod.dev/)-based schema definitions + - `schemas.onSchemaError`: Handler for schema validation errors + - `schemas.abortOnError`: Whether to abort events on validation error (default: `false`) + +### Returns + +`createTracker` returns a tuple with two elements: + +```tsx +const [{ Provider, DOMEvent, Click, Impression, PageView, SetContext }, useTracker] = createTracker({...}) +``` + +#### [Components](./components) + +The first element is an object containing event components: + +```tsx +const [{ Provider, DOMEvent, Click, Impression, PageView, SetContext }] = createTracker(config); +``` + +- `Provider` +- `DOMEvent` +- `Click` +- `Impression` +- `PageView` +- `SetContext` + +#### [Hook](./hook) + +```tsx +const [, useTracker] = createTracker(config); +``` + +The second element is a custom React hook giving access to event tracking functions and context management inside components. diff --git a/docs/src/content/en/docs/data-type-validation.mdx b/docs/src/content/en/docs/data-type-validation.mdx new file mode 100644 index 0000000..7af3e43 --- /dev/null +++ b/docs/src/content/en/docs/data-type-validation.mdx @@ -0,0 +1,98 @@ +import { Callout, Steps } from "nextra/components"; + +# Data Validation + +Event Tracker provides schema-based data type validation using [Zod](https://zod.dev/) to ensure the accuracy of event parameters. +With this feature, events are validated against predefined structures before being collected, helping to ensure data reliability, prevent errors, and provide a foundation for structured analytics. + +## Usage + +Define Zod schemas and pass them to the `schemas` option in the `createTracker` configuration. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; +import { z } from "zod"; + +const schemas = { + page_view: z.object({ + title: z.string(), + }), + click_button: z.object({ + target: z.string(), + }), +}; + +const [Track] = createTracker<{}, {}, typeof schemas>({ + schemas: { + schemas, + onSchemaError: (error) => { + console.error("Schema validation failed:", error); + }, + abortOnError: true, + }, +}); +``` + +## Reference + +| Option | Type | Description | Default | +| --------------- | -------------------------------------------- | ----------------------------------------------------- | -------- | +| `schemas` | `Record` | Mapping of event names to Zod schemas | Required | +| `onSchemaError` | `(error: ZodError) => void \| Promise` | Function called when schema validation fails | Optional | +| `abortOnError` | `boolean` | Whether to abort event processing on validation error | `false` | + + + When `abortOnError` is set to `true`, events that fail schema validation will not be tracked. + + +## Examples + + + +### Define Zod Schemas + +```ts +import { z } from "zod"; + +const schemas = { + page_view: z.object({ + title: z.string(), + }), + click_button: z.object({ + target: z.string(), + }), +}; +``` + +### Register with `createTracker` + +```tsx +const [Track] = createTracker<{}, {}, typeof schemas>({ + schemas: { + schemas, + abortOnError: true, + onSchemaError: (error) => { + console.error("Schema error occurred", error); + }, + }, +}); +``` + +### Schema-Based Event Tracking + +```tsx + + +``` + +If schema validation passes, the event is sent to `send` or the respective handler. +If it fails, `onSchemaError` is triggered, and if `abortOnError` is `true`, the event is discarded. + + + +## Notes + +- Zod schemas use [parse](https://zod.dev/?id=parse) for validation by default. +- For better type inference, define schemas outside of the `createTracker` function and pass `typeof schemas` as the third type argument to `createTracker`. + +_Support for additional schema validation libraries is planned for the future._ diff --git a/docs/src/content/en/docs/hook.mdx b/docs/src/content/en/docs/hook.mdx new file mode 100644 index 0000000..1d8b65b --- /dev/null +++ b/docs/src/content/en/docs/hook.mdx @@ -0,0 +1,233 @@ +import { Callout } from "nextra/components"; + +# Hook + +Inside a React component, you can configure the tracking context or imperatively trigger events. This hook is the second item returned from [`createTracker`](/docs/create-tracker). + +It is primarily used to manually trigger events after a component mounts, upon user input, or after asynchronous tasks complete. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track, useTracker] = createTracker({...}); + +function MyComponent() { + const { setContext, getContext, track, trackWithSchema } = useTracker(); + + // Imperative event tracking +} +``` + +## Reference + +### Returns + +| Property | Type | Description | +| ----------------- | --------------------------------------------------------------- | --------------------------------------------------- | +| `setContext` | `(context: Context \| (prev: Context) => Context) => void` | Sets or updates the tracking context | +| `getContext` | `() => Context` | Returns the current tracking context | +| `track` | `Record void>` | Collection of standard event tracking functions | +| `trackWithSchema` | `Record void>` | Collection of schema-validated event tracking funcs | + +--- + +#### `setContext` + +```tsx +const { setContext } = useTracker(); + +// Set a new context +setContext({ userId: "user-123" }); + +// Update context based on previous value +setContext((prev) => ({ + ...prev, + lastActive: new Date(), +})); +``` + +- Sets or updates the current tracking context. +- Useful for updating user information, session data, etc. + +#### `getContext` + +```tsx +const { getContext } = useTracker(); + +const currentContext = getContext(); +console.log("Current user:", currentContext.userId); +``` + +- Returns the current tracking context. +- Useful for accessing the current tracking state. + +#### `track` + +```tsx +const { track } = useTracker(); + +// Simple click event tracking +track.onClick({ buttonId: "submit" }); + +// Conditional tracking +track.onClick( + { buttonId: "premium" }, + { + enabled: (context) => context.user?.isPremium, + }, +); + +// Tracking with debouncing +track.onClick( + { buttonId: "search" }, + { + debounce: { delay: 300, leading: false, trailing: true }, + }, +); + +// Tracking with throttling +track.onClick( + { buttonId: "rapid-action" }, + { + throttle: { delay: 1000, leading: true, trailing: false }, + }, +); + +// Impression event tracking +track.onImpression({ elementId: "hero" }); +``` + +- An object containing all configured event tracking functions. +- Keys match event names defined in the tracker configuration. +- Supports optional `TrackingOptions` for advanced control. + +#### `trackWithSchema` + +```tsx +const { trackWithSchema } = useTracker(); + +// Track a click event with schema validation +trackWithSchema.onClick({ schema: "click", params: { buttonId: "submit" } }); + +// Conditional tracking with schema +trackWithSchema.onClick( + { + schema: "premium_click", + params: { buttonId: "premium", userId: "123" }, + }, + { + enabled: (context, params) => context.user?.id === params.userId, + }, +); + +// Tracking with schema and throttling +trackWithSchema.onImpression( + { + schema: "impression", + params: { elementId: "hero", userId: "123" }, + }, + { + throttle: { delay: 2000, leading: true, trailing: false }, + }, +); +``` + +- An object containing all schema-validated event tracking functions. +- Keys match event names defined in the tracker configuration. +- Supports optional `TrackingOptions` for advanced control. + +## TrackingOptions + +Both `track` and `trackWithSchema` accept the following optional configuration: + +| Option | Type | Description | +| ---------- | ------------------------------------------- | --------------------------------------------------- | +| `enabled` | `boolean \| ((context, params) => boolean)` | Enables/disables event tracking conditionally | +| `debounce` | `DebounceConfig` | Limits consecutive events (triggers last call only) | +| `throttle` | `ThrottleConfig` | Prevents repeated calls in a short time window | + + + Note: `debounce` and `throttle` are mutually exclusive and cannot be used together. A type error will prevent invalid + usage. + + +#### `DebounceConfig` + +```ts +interface DebounceConfig { + delay: number; + leading?: boolean; // Default: false + trailing?: boolean; // Default: true +} +``` + +#### `ThrottleConfig` + +```ts +interface ThrottleConfig { + delay: number; + leading?: boolean; // Default: true + trailing?: boolean; // Default: false +} +``` + +## Best Practices + +### Context Management + +- Use `setContext` to update global state that affects multiple events. +- Consider the functional form of `setContext` for updates based on previous state. + +### Event Tracking + +- Use the event functions from `track` or `trackWithSchema` wherever possible. + +### Performance Optimization + +- Avoid calling event tracking functions during rendering. +- Use [Batching](/docs/batching) to improve performance. +- Use [Data Validation](/docs/data-type-validation) to ensure data safety. + +## Examples + +### Basic + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track, useTracker] = createTracker({ + onClick: (params) => { + // Send event to analytics service + analytics.track(params); + }, + pageView: { + onPageView: (params) => { + analytics.pageView(params); + }, + }, +}); + +function UserProfile({ userId }) { + const { setContext, track, trackWithSchema } = useTracker(); + + useEffect(() => { + // Update context when userId changes + setContext({ userId }); + + // Track page view event + track.onPageView({ page: "profile" }); + }, [userId]); + + const handleSettingsClick = () => { + // Track user settings event + trackWithSchema.onClick({ schema: "settings", params: { userId } }); + }; + + return ( +
+

User Profile

+ +
+ ); +} +``` diff --git a/docs/src/content/en/docs/installation.mdx b/docs/src/content/en/docs/installation.mdx new file mode 100644 index 0000000..d88a53e --- /dev/null +++ b/docs/src/content/en/docs/installation.mdx @@ -0,0 +1,11 @@ +# Installation + +Event Tracker is a library designed to make implementing event tracking in React applications simple and efficient. It supports `React@^16.8.0` and above. + +To install the latest stable version, run the following command: + +![NPM Version](https://img.shields.io/npm/v/%40offlegacy%2Fevent-tracker) + +```shell npm2yarn +npm install @offlegacy/event-tracker +``` diff --git a/docs/src/content/en/docs/introduction.mdx b/docs/src/content/en/docs/introduction.mdx new file mode 100644 index 0000000..a1af609 --- /dev/null +++ b/docs/src/content/en/docs/introduction.mdx @@ -0,0 +1,83 @@ +# Introduction + +Welcome to the Event Tracker documentation. + +## What is Event Tracker? + +Event Tracker is a declarative React library that simplifies the process of implementing complex event tracking, allowing developers to focus more on business logic. It is designed to make event tracking easy and efficient for applications of any scale. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +// Create a tracker instance +const [Track, useTracker] = createTracker({ + DOMEvents: { + onClick: (params, context) => { + log("Click event:", params, context); + }, + }, +}); + +// Usage in the app +function App() { + return ( + + + + + + ); +} +``` + +### Key Features + +Event Tracker provides a wide range of features focused on both developer experience and application performance. + +| Feature | Description | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Declarative API | Easily declare tracking as components without additional setup or complex code. | +| Analytics Tool Agnostic | Integrate with any tool such as [Google Analytics](https://analytics.google.com/) or [Amplitude](https://amplitude.com/) without modifying existing infrastructure. | +| Clear Separation of Concerns | Keep tracking logic completely separate from business code, improving readability, testability, and maintainability. | +| Optimized Performance | Built-in strategies like batching, debouncing, and throttling minimize network requests, enabling stable tracking without performance degradation. | +| Guaranteed Execution Order | Ensures events are processed in the intended order even in asynchronous scenarios, enabling accurate tracking in complex user flows. | +| Data Validation | Built-in static schema validation with [Zod](https://zod.dev/) prevents build-time errors and ensures the reliability of collected event data. | + +## Core Concepts + +To use Event Tracker effectively, there are several key concepts to understand. + +### Instance (`createTracker`) + +This is the fundamental starting point of the library. Using the `createTracker` function, you create a tracker instance (a collection of `Track` components and the `useTracker` hook). Here, you define event tracking by configuring DOM event handlers, impression handlers, and schemas. + +You can create multiple tracker instances for different purposes (for example, one for Google Analytics and another for Amplitude). + +### Provider (`Track.Provider`) + +Implemented based on React’s Context API. Wrap the top level of your application or a specific component tree with `Track.Provider` to supply common data (context) needed for tracking to child components. For example, passing `userId` or `pageName` as context allows these values to be used during event tracking. + +### Event Components (`Track.Click`, `Track.PageView`, etc.) + +These are special components that enable declarative event tracking. They are provided as the first element in the array returned by `createTracker`. + +- `Track.Click`: Tracks when a click event occurs on child elements. +- `Track.Impression`: Tracks when child elements are displayed on screen. +- `Track.PageView`: Tracks a page view event when the component mounts. + +You can use the provided components or create custom ones to handle various user interactions and lifecycle events. Each component accepts `context` and `params` props to pass specific data relevant to the event. + +### Custom Hook + +Use when you need more complex or conditional event tracking that is not directly tied to component lifecycle or DOM events. The hook allows you to access context from `Track.Provider` and execute defined tracking logic imperatively. + +## Next Steps + +These core concepts work together to create a powerful and flexible event tracking environment with Event Tracker. For more detailed usage and in-depth information on each feature, see the following documentation: + +- [Why Event Tracker?](/docs/why-event-tracker): Learn the necessity and main features of Event Tracker. +- [`createTracker`](/docs/create-tracker): Learn how to create and configure a tracker instance. +- [Components](/docs/components): Explore all available tracking components with usage examples. +- [Hook](/docs/hook): Learn how to implement custom tracking with the hook. +- [Batching](/docs/batching): Optimize performance through event batching strategies. +- [Data Validation](/docs/data-type-validation): Ensure data integrity with Zod schema validation. diff --git a/docs/src/content/en/docs/why-event-tracker.mdx b/docs/src/content/en/docs/why-event-tracker.mdx new file mode 100644 index 0000000..101790f --- /dev/null +++ b/docs/src/content/en/docs/why-event-tracker.mdx @@ -0,0 +1,196 @@ +import { Steps } from "nextra/components"; + +# Why Event Tracker? + +Modern web applications must continuously analyze user behavior to improve service quality. However, traditional event tracking approaches come with several challenges. + +## Why You Need Event Tracker + +The following example highlights common issues with conventional event tracking. + +```tsx {8,15, 24-31} +function Page() { + const { user, userId } = useUser(); // Fetch user info and ID. + + return ( +
+

User: {user.name}

+ {/* Passing userId to Counter solely for event tracking */} + +
+ ); +} + +// Counter receives 'userId' only for tracking purposes. +// If nested deeper, this causes severe prop drilling. +function Counter({ userId }: { userId: string }) { + const [count, setCount] = useState(0); + const { track } = useTrackEvent(); // Hypothetical tracking hook + + const handleIncrement = () => { + const newCount = count + 1; + setCount(newCount); + + // Business logic (count increment) mixed with tracking logic. + track({ + event: "click_increment", + params: { + type: "count", + value: newCount, + userId, // userId passed down from parent + }, + }); + }; + + return ( +
+

Count: {count}

+ +
+ ); +} +``` + + + +### The Pain of Prop Drilling + +To pass tracking-related data down to nested components, you often end up drilling props through multiple layers. This reduces code readability and complicates maintenance. + +### Tight Coupling of Logic + +Mixing business logic with tracking logic increases code complexity and makes independent testing and modification harder. + +### Increased Boilerplate + +Repetitive tracking code reduces developer productivity. + + + +## A New Paradigm with Event Tracker + +Event Tracker introduces a new paradigm for event tracking. Its declarative approach simplifies the inherent complexity of traditional tracking and makes it accessible to all developers. + + + +### Declarative Event Tracking + +```tsx {7, 10, 12, 32, 34} +function Page() { + const { user, userId } = useUser(); + + // Provide tracking context (userId) to child components via Track.Provider. + // No more prop drilling required. + return ( + +
+

User: {user.name}

+ {/* No need to pass userId as a prop */} +
+
+ ); +} + +function Counter() { + const [count, setCount] = useState(0); + + const handleIncrement = () => { + // handleIncrement is now purely responsible for incrementing count. + setCount(count + 1); + }; + + return ( +
+

Count: {count}

+ {/* + Track.Click wraps the button and tracks the click event + with the defined parameters. userId from context is automatically + included in the tracking data. + */} + + + +
+ ); +} +``` + +With Event Tracker, declarative event tracking improves code readability and reduces complexity. This helps developers focus on _what_ to track rather than _how_ to track. +The `handleIncrement` function now handles only count logic, while `` manages tracking. +**This declarative approach shifts the developer’s focus from implementation details to defining the intent of tracking.** +How tracking is handled is defined outside the React app. + +### Improved Tracking Cohesion + +```tsx {4-10, 14-20} +const [Track, useTracker] = createTracker({ + // Callback for DOM events + DOMEvents: { + onClick: (params, context) => { + // Call actual tracking tools (Google Analytics, Amplitude, etc.) + logEvent("click_event", { + ...params, // { value: ..., type: "count" } + userId: context.userId, // Provided by the Provider + }); + }, + // You can also define onMouseOver, onFocus, etc. + }, + // Callback for Impression events + onImpression: (params, context) => { + logEvent("impression_event", { + ...params, + userId: context.userId, + pagePath: window.location.pathname, + }); + }, +}); +``` + +Now, the code defining _how to track_ is separated from business logic. Since it's located outside the application, you can modify tracking logic without touching business code. + +### Data Validation + +```tsx {13-20, 24-33} +import { z } from "zod"; +import { createTracker } from "@offlegacy/event-tracker"; + +interface Context { + /* ... */ +} + +interface Params { + /* ... */ +} + +// Define schemas +const schemas = { + page_view: z.object({ + title: z.string(), + }), + click_button: z.object({ + target: z.string(), + }), +}; + +// Configure tracker +const [Track] = createTracker({ + schema: { + schemas: { + page_view, + click_button, + }, + onSchemaError: (error) => { + console.error("Schema validation error:", error); + }, + abortOnError: true, + }, +}); + +// Using schemas +; +; +``` + +Event Tracker integrates with [Zod](https://zod.dev/) to provide robust schema-based data type validation. This prevents data errors during development and enhances the reliability of tracking data. + +
diff --git a/docs/src/content/en/docs/with-google-analytics-example.mdx b/docs/src/content/en/docs/with-google-analytics-example.mdx new file mode 100644 index 0000000..4b449a6 --- /dev/null +++ b/docs/src/content/en/docs/with-google-analytics-example.mdx @@ -0,0 +1,115 @@ +import { Steps } from "nextra/components"; + +# Integrating with Google Analytics + +This guide walks you through integrating GA4 (Google Analytics 4) to track user behavior step by step. + + + +## Set Up GA4 Script + +Add the GA4 script to the `` of your `index.html` to enable `gtag`. + +```html + + + + + +``` + +## Define `sendGA4` Function + +Create a wrapper function to safely call `gtag` with validation and warnings. + +```ts +export function sendGA4(eventName: string, eventParams: Record = {}): void { + if (typeof window === "undefined") return; + if (typeof window.gtag !== "function") { + console.warn("[GA4] gtag function is not loaded."); + return; + } + window.gtag("event", eventName, eventParams); +} +``` + +## Define Tracking Events + +Configure `createTracker` with GA4 event sending logic and set up DOM, impression, and page view events. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; +import { sendGA4 } from "./sendGA4"; + +interface GA4Context { + userId: string; +} + +interface GA4EventParams { + eventName: string; +} + +export const [Track, useTracker] = createTracker({ + DOMEvents: { + onClick: (params, ctx) => sendGA4(params.eventName, { ...params, user_id: ctx.userId }), + }, + impression: { + onImpression: (params, ctx) => sendGA4("impression", { ...params, user_id: ctx.userId }), + options: { threshold: 0.5, freezeOnceVisible: true }, + }, +}); +``` + +## Use in a Blog Page + +Wrap your app in `Track.Provider` and use ``, ``, and `` components to declaratively record events. + +```tsx +import { Track } from "./event-tracker"; + +function App() { + return ( + + + + ); +} + +interface PostProps { + id: number; + title: string; + slug: string; + content: string; +} + +function Post({ post }: PostProps) { + return ( +
+

{post.title}

+ + +
{post.content}
+
+ +
+ + + + + + + +
+
+ ); +} +``` + +
diff --git a/docs/src/content/en/docs/with-zod-example.mdx b/docs/src/content/en/docs/with-zod-example.mdx new file mode 100644 index 0000000..c1aec6e --- /dev/null +++ b/docs/src/content/en/docs/with-zod-example.mdx @@ -0,0 +1,72 @@ +import { Steps } from "nextra/components"; + +# Using with Zod + +Learn how to implement powerful data type validation by integrating [Zod](https://zod.dev/). +Schema-based validation with Zod helps prevent errors before runtime and ensures data reliability. + + + +## Define Schemas + +Declare event-specific fields and types using Zod objects. + +```tsx {4-5} +import { z } from "zod"; + +export const schemas = { + page_view: z.object({ title: z.string() }), + click_button: z.object({ target: z.string() }), +}; +``` + +## Define Tracking Events + +Connect the schemas to `createTracker` to automatically validate event parameters. + +```tsx {5-9} +import { createTracker } from "@offlegacy/event-tracker"; +import { schemas } from "./schemas"; + +export const [Track, useTracker] = createTracker<{}, {}, typeof schemas>({ + schemas: { + schemas, + onSchemaError: (error) => console.error("Schema validation failed:", error), + abortOnError: true, + }, +}); +``` + +## Use Validated Events + +Send type-safe events via declarative components and the `track` function. + +```tsx {11, 17-19} +import { Track } from "./event-tracker"; + +function HomePage() { + return ( +
+

Homepage

+ + {/* ✅ Valid with Zod schema, no type error */} + + + {/* 🚨 Invalid with Zod schema, type error */} + {/* */} + + {/* ✅ Valid with Zod schema, no type error */} + + + + + {/* 🚨 Invalid with Zod schema, type error */} + {/* + + */} +
+ ); +} +``` + +
diff --git a/docs/src/content/en/hook.mdx b/docs/src/content/en/hook.mdx deleted file mode 100644 index 0c8c4e2..0000000 --- a/docs/src/content/en/hook.mdx +++ /dev/null @@ -1,223 +0,0 @@ -# hook - -A custom React hook returned as the second array item from [`createTracker`](/docs/create-tracker). -It provides access to tracking functionality and context management within your components. - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -const [Track, useTracker] = createTracker({...}) - -function MyComponent() { - const { setContext, getContext, track, trackWithSchema } = useTracker(); - - return ( - // Your component content - ); -} -``` - -### Return Value - -The hook returns an object with the following properties: - -#### setContext - -- Type: `(context: Context) => void` -- Sets or updates the current tracking context -- Can be used to update user information, session data, etc. - -```tsx -const { setContext } = useTracker(); - -// Set new context -setContext({ userId: "user-123" }); - -// Update context based on previous value -setContext((prev) => ({ - ...prev, - lastActive: new Date(), -})); -``` - -#### getContext - -- Type: `() => Context` -- Returns the current tracking context -- Useful for accessing current tracking state - -```tsx -const { getContext } = useTracker(); - -const currentContext = getContext(); -console.log("Current user:", currentContext.userId); -``` - -#### track - -- Type: `Record void>` -- Object containing all configured event tracking functions -- Keys match the event names defined in your tracker configuration -- Accepts optional `TrackingOptions` for advanced control - -```tsx -const { track } = useTracker(); - -// Track a simple click event -track.onClick({ buttonId: "submit" }); - -// Track with conditional logic -track.onClick( - { buttonId: "premium" }, - { - enabled: (context) => context.user?.isPremium, - }, -); - -// Track with debouncing -track.onClick( - { buttonId: "search" }, - { - debounce: { delay: 300, leading: false, trailing: true }, - }, -); - -// Track with throttling -track.onClick( - { buttonId: "rapid-action" }, - { - throttle: { delay: 1000, leading: true, trailing: false }, - }, -); - -// Track an impression -track.onImpression({ elementId: "hero" }); -``` - -#### trackWithSchema - -- Type: `Record void>` -- Object containing all configured event tracking functions with schema validation -- Keys match the event names defined in your tracker configuration -- Accepts optional `TrackingOptions` for advanced control - -```tsx -const { trackWithSchema } = useTracker(); - -// Track a click event with schema -trackWithSchema.onClick({ schema: "click", params: { buttonId: "submit" } }); - -// Track with conditional logic and schema -trackWithSchema.onClick( - { - schema: "premium_click", - params: { buttonId: "premium", userId: "123" }, - }, - { - enabled: (context, params) => context.user?.id === params.userId, - }, -); - -// Track with throttling and schema -trackWithSchema.onImpression( - { - schema: "impression", - params: { elementId: "hero", userId: "123" }, - }, - { - throttle: { delay: 2000, leading: true, trailing: false }, - }, -); -``` - -### TrackingOptions - -Both `track` and `trackWithSchema` methods accept an optional second parameter with the following options: - -- `enabled?: boolean | ((context: Context, params: EventParams) => boolean)` - Conditionally enable/disable event tracking -- `debounce?: DebounceConfig` - Debounce configuration to prevent rapid successive events -- `throttle?: ThrottleConfig` - Throttle configuration to limit event frequency - -**Note:** `debounce` and `throttle` are mutually exclusive and cannot be used together. - -#### DebounceConfig - -```tsx -interface DebounceConfig { - delay: number; // Delay in milliseconds - leading?: boolean; // Execute on leading edge (default: false) - trailing?: boolean; // Execute on trailing edge (default: true) -} -``` - -#### ThrottleConfig - -```tsx -interface ThrottleConfig { - delay: number; // Delay in milliseconds - leading?: boolean; // Execute on leading edge (default: true) - trailing?: boolean; // Execute on trailing edge (default: false) -} -``` - -### Example Usage - -Here's a complete example showing how to use the custom hook: - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -const [Track, useTracker] = createTracker({ - onClick: (params) => { - // Send event to analytics service - analytics.track(params); - }, - pageView: { - onPageView: (params) => { - // Send event to analytics service - analytics.pageView(params); - }, - }, -}); - -function UserProfile({ userId }) { - const { setContext, track, trackWithSchema } = useTracker(); - - useEffect(() => { - // Update context when user ID changes - setContext({ userId }); - - // Track page view - track.onPageView({ page: "profile" }); - }, [userId]); - - const handleSettingsClick = () => { - // Track custom event - trackWithSchema.onClick({ schema: "settings", params: { userId } }); - }; - - return ( -
-

User Profile

- -
- ); -} -``` - -### Best Practices - -1. **Context Updates** - - - Use `setContext` for global state that affects multiple events - - Consider using the function form of `setContext` for updates based on previous state - -2. **Event Tracking** - - - Use the specific event functions from `track` or `trackWithSchema` when possible - -3. **Performance** - - Avoid calling tracking functions in render - - Use callbacks or effects for tracking - - Consider using [batching](/docs/batching) for better performance - - Consider using [data type validation](/docs/data-type-validation) for data type safety diff --git a/docs/src/content/en/index.mdx b/docs/src/content/en/index.mdx index 2a87bb1..ee84f82 100644 --- a/docs/src/content/en/index.mdx +++ b/docs/src/content/en/index.mdx @@ -1,202 +1,42 @@ -# Introduction - -## Motivation - -Event tracking is a complex task that requires a lot of boilerplate code and complexity. -Take a look at this example. - -```tsx {14,15, 23-30} -// Traditional event tracking - -const Page = () => { - const { user, userId } = useUser(); - - return ( -
-

User: {user.name}

- -
- ); -}; - -const Counter = ({ userId }: { userId: string }) => { - // Receives 'userId' as a prop just for event tracking purposes. - - const [count, setCount] = useState(0); - const { track } = useTrackEvent(); - - const handleIncrement = () => { - setCount(count + 1); - - track({ - event: "click", - params: { - type: "count", - value: count + 1, - userId, - }, - }); - }; - - return ( -
-

Count: {count}

- -
- ); -}; -``` - -The two main inconveniences caused by event tracking in the code are: - -1. **Prop Drilling**: The `userId` is passed as a prop to the `` component from the `` component. If the `` component is nested deeper within the component tree, prop drilling can become more severe, reducing the readability and maintainability of the code. -2. **Coupling of Event Tracking Logic and Business Logic**: The `handleIncrement` function contains both the logic for incrementing the count and tracking the event. Separating the event tracking logic can make the code cleaner and easier to maintain. - -## The New Paradigm - -`event-tracker` introduces a new paradigm for event tracking. -The declarative nature of this paradigm simplifies -the complexity traditionally associated with event tracking, making it accessible to developers of all skill levels. - -### Declarative event tracking - -```tsx {5, 10, 24, 26} -const Page = () => { - const { user, userId } = useUser(); - - return ( - -
-

User: {user.name}

- -
-
- ); -}; - -const Counter = () => { - const [count, setCount] = useState(0); - - const handleIncrement = () => { - setCount(count + 1); - }; - - return ( -
-

Count: {count}

- - - -
- ); -}; -``` - -Using `event-tracker` allows for declarative event tracking, which improves code readability and reduces complexity. This helps developers understand and use event tracking more easily. -Your `handleIncrement` function is now only responsible for incrementing the count, and the event tracking is handled by the `` component. -**This declarative approach makes the developer focus on _'what to track'_ and not _'how to track'._** -How to track should be defined outside of the React app. - -### Improve code cohesion outside your application. - -```tsx -const [Track] = createTracker({ - DOMEvents: { - onClick: (params, context) => { - log({ - event: "click", - params: { - ...params, - userId: context.userId, - }, - }); +--- +title: Event Tracker | Comprehensive solution for event tracking in React applications +--- + +import HomePage from "@/components/home-page"; + + { - log({ - event: "impression", - params: { - ...params, - userId: context.userId, - }, - }); - }, -}); -``` - -Your instructions for **'how to track'** is now separated from your business logic. -It's located outside your application, so you can change your event tracking provider without having to change your application code. - -### Data Type Validation - -`event-tracker` provides built-in schema validation using [Zod](https://zod.dev/), a TypeScript-first schema validation library. - -```tsx -import { z } from "zod"; -import { createTracker } from "@offlegacy/event-tracker"; - -interface Context { - // ... -} - -interface Params { - // ... -} - -// Define Schemas -const schemas = { - page_view: z.object({ - title: z.string(), - }), - click_button: z.object({ - target: z.string(), - }), -}; - -// Configure Tracker -const [Track] = createTracker({ - // other configurations... - - schema: { - schemas: { - page_view, - click_button, + { + title: "Analytics Tool Independence", + description: + "Easily integrates with tools like Google Analytics, Amplitude, or Segment—without forcing you to switch or couple to a specific vendor.", }, - onSchemaError: (error) => { - console.error("Schema validation error:", error); + { + title: "Clear Separation of Concerns", + description: + "Keeps tracking logic out of your business logic, resulting in cleaner code that’s easier to test, maintain, and evolve.", }, - abortOnError: true, - }, -}); - -// Use the schemas -; -; -``` - -## Key Features - -- 🎯 **Type-safe APIs**: Declarative event tracking with complete type safety -- 🛡️ **Data Type Validation**: Ensures data type safety and consistency using schemas -- ⚡️ **Optimized Performance**: Enhanced performance through event batching -- 🔄 **Guaranteed Order**: Guaranteed execution order for async operations -- 🔌 **Analytics Agnostic**: Works with any analytics provider of your choice -- 🧩 **Clean Separation**: Keeps tracking logic separate from business logic -- 📦 **Lightweight**: Minimal bundle size impact on your application - -## Basic Concepts - -`event-tracker` is built around a few core concepts: - -1. **Tracker Creation**: Use `createTracker` to create a tracker instance with your desired event tracking instructions. -2. **Provider**: Wrap your app with `Track.Provider` to provide context. -3. **Event Components**: Use components like `Track.Click` or `Track.Impression` to track events. -4. **Hooks**: Use the `useTracker` hook to access tracking functionality in your components imperatively. - -Check out the other sections for detailed documentation on each feature: - -- [createTracker](/docs/create-tracker) - Detailed API documentation on `createTracker` -- [components](/docs/components) - Available tracking components -- [hook](/docs/hook) - Track using the hook -- [Batching](/docs/batching) - Optimizing performance with event batching -- [Data Type Validation](/docs/data-type-validation) - Ensuring data type safety and consistency using schemas + { + title: "Optimized Performance", + description: + "Built-in strategies like batching, debouncing, and throttling minimize network overhead, enabling efficient and reliable tracking.", + }, + { + title: "Guaranteed Execution Order", + description: + "Ensures that events are processed in the intended order, even under asynchronous conditions—crucial for accurate tracking in complex flows.", + }, + { + title: "Powerful Data Validation", + description: + "Schema-based validation with Zod catches issues before runtime and guarantees the integrity of your event data.", + }, + ]} +/> diff --git a/docs/src/content/en/installation.mdx b/docs/src/content/en/installation.mdx deleted file mode 100644 index d2c9a98..0000000 --- a/docs/src/content/en/installation.mdx +++ /dev/null @@ -1,19 +0,0 @@ -# Installation - -Using npm: - -```bash -npm install @offlegacy/event-tracker -``` - -Using yarn: - -```bash -yarn add @offlegacy/event-tracker -``` - -Using pnpm: - -```bash -pnpm add @offlegacy/event-tracker -``` diff --git a/docs/src/content/en/quick-start.mdx b/docs/src/content/en/quick-start.mdx deleted file mode 100644 index 0a5728b..0000000 --- a/docs/src/content/en/quick-start.mdx +++ /dev/null @@ -1,27 +0,0 @@ -# Quick Start - -Here's a simple example of how to use event-tracker: - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -// Create a tracker instance -const [Track, useTracker] = createTracker({ - DOMEvents: { - onClick: (params, context) => { - log("Click event:", params, context); - }, - }, -}); - -// Use in your app -function App() { - return ( - - - - - - ); -} -``` diff --git a/docs/src/content/ko/_meta.ts b/docs/src/content/ko/_meta.ts deleted file mode 100644 index 2a81597..0000000 --- a/docs/src/content/ko/_meta.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { MetaRecord } from "nextra"; - -const meta: MetaRecord = { - "getting-started-separator": { - type: "separator", - title: "시작하기", - }, - index: { - title: "소개", - theme: { - toc: true, - layout: "default", - }, - }, - installation: { - title: "설치", - theme: { - toc: true, - layout: "default", - }, - }, - "quick-start": { - title: "가이드", - theme: { - toc: true, - layout: "default", - }, - }, - "api-separator": { - title: "API", - type: "separator", - }, - "create-tracker": { - title: "createTracker", - theme: { - toc: true, - layout: "default", - }, - }, - components: { - title: "컴포넌트", - theme: { - toc: true, - layout: "default", - }, - }, - hook: { - title: "hook", - theme: { - toc: true, - layout: "default", - }, - }, - "advanced-separator": { - title: "고급", - type: "separator", - }, - batching: { - title: "배칭", - theme: { - toc: true, - layout: "default", - }, - }, -}; - -export default meta; diff --git a/docs/src/content/ko/_meta.tsx b/docs/src/content/ko/_meta.tsx new file mode 100644 index 0000000..6fef8c5 --- /dev/null +++ b/docs/src/content/ko/_meta.tsx @@ -0,0 +1,16 @@ +import type { MetaRecord } from "nextra"; + +export default { + index: { + type: "page", + display: "hidden", + theme: { + layout: "full", + toc: false, + }, + }, + docs: { + type: "page", + title: "문서보기", + }, +} satisfies MetaRecord; diff --git a/docs/src/content/ko/batching.mdx b/docs/src/content/ko/batching.mdx deleted file mode 100644 index e003e7f..0000000 --- a/docs/src/content/ko/batching.mdx +++ /dev/null @@ -1,128 +0,0 @@ -# 배칭 - -## 개요 - -이벤트 배칭은 여러 이벤트를 묶어서 애널리틱스 서비스에 보내기 전에 진행하는 성능 최적화 기술입니다. 이는 다음과 같은 경우에 특히 유용합니다: - -- 내부 배칭 기능이 있는 애널리틱스 서비스(Amplitude 또는 Google Analytics)를 사용하지 않는 경우 -- 네트워크 요청 수를 줄이고 싶은 경우 -- 성능을 최적화하고 싶은 경우 - -## 설정 - -트래커 설정에서 배칭을 활성화합니다: - -```tsx -const [Track, useTracker] = createTracker({ - // ... other config - batch: { - enable: true, - interval: 2000, - thresholdSize: 100, - onFlush: (batch, isBrowserClosing) => { - if (isBrowserClosing) { - // 브라우저가 닫힐 때 신뢰할 수 있는 전송을 위해 sendBeacon 사용 - navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); - return; - } - - // 일반 배칭 처리 - fetch("/analytics/collect/batch", { - method: "POST", - body: JSON.stringify(batch), - }); - }, - }, -}); -``` - -### 옵션 - -- `enable: boolean` (기본값: `false`) - - 배칭 활성화/비활성화 -- `interval: number` (기본값: `3000`) - - 배칭 처리 간격 (ms) -- `thresholdSize: number` (기본값: `25`) - - 배칭 처리 전 최대 이벤트 수 -- `onFlush: (batch: Batch[], isBrowserClosing: boolean) => void | Promise` - - 배칭 활성화 시 필요 - - 배칭 처리 함수 - - `isBrowserClosing` 브라우저 닫힘 시 배칭 처리 여부 -- `onError: (error: Error) => void | Promise` - - 배칭 처리 실패 시 오류 처리 함수 - -## 작동 방식 - -1. 이벤트는 메모리에 수집됩니다 -2. 다음 중 하나가 발생할 때 배칭이 처리됩니다: - - 구성된 간격이 도달합니다 - - 배칭 크기가 임계치에 도달합니다 - - 브라우저가 닫힐 때 - -### 예시 - -```tsx -const [Track, useTracker] = createTracker({ - DOMEvents: { - onClick: (clickParams, context) => { - // 반환 값이 배칭됩니다 - return { - ...clickParams, - ...context, - event_type: "click", - timestamp: new Date().getTime(), - }; - }, - }, - batch: { - enable: true, - interval: 2000, - thresholdSize: 100, - onFlush: (batch, isBrowserClosing) => { - // Process the batch - }, - }, -}); - -function App() { - return ( - - - - - - ); -} -``` - -버튼을 여러 번 클릭했을 때: - -1. 각 클릭은 `onClick` 핸들러를 실행합니다 -2. 각 핸들러의 반환 값은 배치 큐에 추가됩니다 -3. 2초(또는 임계치에 도달할 때)가 지나면 `onFlush`가 모든 수집된 이벤트와 함께 호출됩니다 - -## 브라우저 닫힘 시 동작 - -브라우저가 닫힐 때, [`navigator.sendBeacon`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon)을 사용하여 이벤트 전송을 보장합니다: - -```tsx -onFlush: (batch, isBrowserClosing) => { - if (isBrowserClosing) { - navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); - return; - } - - // 일반 배칭 처리 - fetch("/analytics/collect/batch", { - method: "POST", - body: JSON.stringify(batch), - }); -}; -``` - -## 유의 사항 - -1. 이벤트 핸들러의 **반환 값**만 배칭되며, 핸들러 자체는 배칭되지 않습니다 -2. 배칭된 이벤트는 원래 매개변수와 컨텍스트를 유지합니다 -3. 이벤트는 순서대로 처리되며, 사용자 작업의 순서를 유지합니다 -4. 브라우저 닫힘 시 이벤트 전송을 보장하기 위해 [`navigator.sendBeacon`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon)을 사용합니다 diff --git a/docs/src/content/ko/components/click.mdx b/docs/src/content/ko/components/click.mdx deleted file mode 100644 index 96469f7..0000000 --- a/docs/src/content/ko/components/click.mdx +++ /dev/null @@ -1,85 +0,0 @@ -# Click - -클릭 이벤트(`type="onClick"`)를 위한 `DOMEvent`의 특수 버전입니다. - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -const [Track] = createTracker({ - DOMEvents: { - onClick: (params, context) => { - // 클릭 이벤트 처리 - }, - }, -}); - -function App() { - return ( - - - - - - ); -} -``` - -### Props - -- 스키마와 함께 사용하는 경우 - - `params: SchemaParams | (context: Context) => SchemaParams` - 스키마 기반 클릭 이벤트 매개변수 - - `schema: string` - 이벤트 매개변수 검증을 위한 스키마 이름 -- 스키마와 함께 사용하지 않는 경우 - - `params: EventParams | (context: Context) => EventParams` - 클릭 이벤트 매개변수 -- `enabled?: boolean | ((context: Context, params: EventParams) => boolean)` - 이벤트 추적을 조건부로 활성화/비활성화 (기본값: `true`) -- `debounce?: DebounceConfig` - 연속적인 이벤트 발생을 방지하는 디바운스 설정 -- `throttle?: ThrottleConfig` - 이벤트 발생 빈도를 제한하는 스로틀 설정 - -**참고:** `debounce`와 `throttle`은 상호 배타적이며 함께 사용할 수 없습니다. - -### 추적 옵션 예제 - -#### 조건부 추적 - -```tsx - context.user?.isPremium}> - - -``` - -#### 디바운스 추적 - -```tsx - - - -``` - -#### 스로틀 추적 - -```tsx - - - -``` - -## 이벤트 이름을 변경한 경우 - -`eventName` 속성을 사용하면 사용자 정의 클릭 이벤트 핸들러 이름을 지정할 수 있습니다. -이 속성을 사용하면 핸들러 이름이 변경되어도 유연하게 대처할 수 있습니다. - -```tsx {8, 10} -function MyButton({ onButtonClick }: { onButtonClick?: () => void }) { - return + + + ); +} +``` + + diff --git a/docs/src/content/ko/docs/batching.mdx b/docs/src/content/ko/docs/batching.mdx new file mode 100644 index 0000000..656ffcb --- /dev/null +++ b/docs/src/content/ko/docs/batching.mdx @@ -0,0 +1,129 @@ +import { Callout } from "nextra/components"; + +# 배칭 + +이벤트 배칭은 여러 개의 이벤트를 일정 조건 하에 하나의 네트워크 요청으로 묶어 전송하는 기능입니다. 다음과 같은 경우에 유용하게 사용할 수 있습니다. + +- Amplitude, Google Analytics 등 내부 배칭 기능이 없는 애널리틱스 서비스를 사용하는 경우 +- 네트워크 요청 수를 줄이고 싶은 경우 +- 브라우저 종료 시 이벤트 유실을 방지하고 싶은 경우 + +## Usage + +`createTracker` 설정에서 `batch` 옵션을 활성화하면 사용할 수 있습니다. + +```tsx +const [Track, useTracker] = createTracker({ + // ... 다른 설정 + batch: { + enable: true, + interval: 2000, + thresholdSize: 100, + onFlush: (batch, isBrowserClosing) => { + if (isBrowserClosing) { + navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); + return; + } + + fetch("/analytics/collect/batch", { + method: "POST", + body: JSON.stringify(batch), + }); + }, + }, +}); +``` + +## Options + +| 옵션 | 타입 | 설명 | 기본값 | +| --------------- | ---------------------------------------------------- | ------------------------------------------ | ------- | +| `enable` | `boolean` | 배칭 기능 활성화 여부 | `false` | +| `interval` | `number` | 배치 전송 간격 (밀리초 단위) | `3000` | +| `thresholdSize` | `number` | 배치 전송 전에 쌓일 수 있는 최대 이벤트 수 | `25` | +| `onFlush` | `(batch, isBrowserClosing) => void \| Promise` | 배치 전송 처리 함수. 필수 | - | +| `onError` | `(error: Error) => void \| Promise` | 전송 중 오류 발생 시 호출되는 함수 (선택) | - | + +`onFlush`는 **필수 항목**이며, 배치된 이벤트를 실제로 어떻게 전송할지를 정의합니다. + +## How It Works + +1. 각 이벤트의 **핸들러 반환값**은 메모리에 수집됩니다. +2. 아래 중 하나가 발생하면 `onFlush`가 호출됩니다: + + - `interval` 시간이 경과했을 때 + - 수집된 이벤트 수가 `thresholdSize`에 도달했을 때 + - 브라우저가 닫히는 이벤트가 감지되었을 때 + +## Examples + +```tsx +const [Track, useTracker] = createTracker({ + DOMEvents: { + onClick: (params, context) => { + return { + ...params, + ...context, + event_type: "click", + timestamp: Date.now(), + }; + }, + }, + batch: { + enable: true, + interval: 2000, + thresholdSize: 100, + onFlush: (batch, isBrowserClosing) => { + if (isBrowserClosing) { + navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); + return; + } + + fetch("/analytics/collect/batch", { + method: "POST", + body: JSON.stringify(batch), + }); + }, + }, +}); + +function App() { + return ( + + + + + + ); +} +``` + +1. ``이 실행될 때마다 이벤트 핸들러가 반환하는 객체가 배치 큐에 저장됩니다. +2. 2초 후 또는 100개의 이벤트가 쌓이면 `onFlush`가 호출되어 서버로 전송됩니다. +3. 브라우저가 닫히는 시점에서는 `sendBeacon`으로 전송됩니다. + +## 브라우저 종료 시 처리 + +브라우저가 닫힐 때도 이벤트 유실을 방지하기 위해 +[`navigator.sendBeacon`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon)을 활용할 수 있습니다: + +```tsx +onFlush: (batch, isBrowserClosing) => { + if (isBrowserClosing) { + navigator.sendBeacon("/analytics/collect/batch", JSON.stringify(batch)); + return; + } + + fetch("/analytics/collect/batch", { + method: "POST", + body: JSON.stringify(batch), + }); +}; +``` + +## Notes + +1. **이벤트 핸들러의 반환값만 배칭됩니다.** 이벤트 핸들러 자체는 항상 호출됩니다. +2. **배치된 이벤트는 컨텍스트 정보를 포함한 상태로 저장됩니다.** +3. **전송 순서를 보장합니다.** 클릭 → 노출 순으로 발생한 이벤트는 그 순서대로 전송됩니다. +4. 브라우저 종료 시 `isBrowserClosing` 플래그가 `true`로 설정되어 `onFlush`에서 분기 처리할 수 있습니다. diff --git a/docs/src/content/ko/components/_meta.ts b/docs/src/content/ko/docs/components/_meta.ts similarity index 100% rename from docs/src/content/ko/components/_meta.ts rename to docs/src/content/ko/docs/components/_meta.ts diff --git a/docs/src/content/ko/docs/components/click.mdx b/docs/src/content/ko/docs/components/click.mdx new file mode 100644 index 0000000..d4c1784 --- /dev/null +++ b/docs/src/content/ko/docs/components/click.mdx @@ -0,0 +1,138 @@ +import { Callout } from "nextra/components"; + +# Click + +Click 이벤트(`type="onClick"`)를 위한 `DOMEvent`의 전용 컴포넌트입니다. `createTracker`의 리턴 배열의 첫 번쨰 요소인 이벤트 컴포넌트 중 하나입니다. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ + DOMEvents: { + onClick: (params, context) => { + // 클릭 이벤트 처리 + }, + }, +}); + +function App() { + return ( + + + + + + ); +} +``` + +## Reference + +### Props + +| Props | 타입 | 설명 | 필수 | +| ---------- | ----------------------------------------------------------------- | ------------------------------------------------------- | ---- | +| `enabled` | `boolean \| ((context: Context, params: EventParams) => boolean)` | 이벤트 추적을 조건부로 활성화/비활성화 (기본값: `true`) | - | +| `debounce` | `DebounceConfig` | 연속적인 이벤트 발생을 방지하는 디바운스 설정 | - | +| `throttle` | `ThrottleConfig` | 이벤트 발생 빈도를 제한하는 스로틀 설정 | - | +| `schema` | `string` | 이벤트 매개변수 검증을 위한 스키마 이름 | O | + + + 참고: `debounce`와 `throttle`은 상호 배타적이며 함께 사용할 수 없습니다. 타입 에러를 발생시켜 사전에 방지합니다. + + +스키마와 함께 사용하는 경우와 아닐 경우 타입이 달라지는 속성이 있습니다. + +스키마가 있을 경우: + +| Props | 타입 | 설명 | +| -------- | ---------------------------------------------------- | -------------------- | +| `params` | `SchemaParams \| (context: Context) => SchemaParams` | 스키마 기반 매개변수 | + +스키마가 없을 경우: + +| Props | 타입 | 설명 | +| -------- | -------------------------------------------------- | --------------- | +| `params` | `EventParams \| (context: Context) => EventParams` | 이벤트 매개변수 | + +--- + +#### `params` (필수) + +스키마와 함께 사용하는 경우: + +- 타입: `SchemaParams | (context: Context) => SchemaParams` +- 설명: 스키마 기반 매개변수 + +스키마와 함께 사용하지 않는 경우: + +- 타입: `EventParams | (context: Context) => EventParams` +- 설명: 이벤트 매개변수 + +#### `enabled` + +- 타입: `boolean | ((context: Context, params: EventParams) => boolean)` +- 설명: 이벤트 추적을 조건부로 활성화/비활성화 (기본값: `true`) + +#### `debounce` + +- 타입: `DebounceConfig` +- 설명: 연속적인 이벤트 발생을 방지하는 디바운스 설정 + +#### `throttle` + +- 타입: `ThrottleConfig` +- 설명: 이벤트 발생 빈도를 제한하는 스로틀 설정 + +#### `schema` + +스키마와 함께 사용하는 경우: + +- 타입: `string` +- 설명: 이벤트 매개변수 검증을 위한 스키마 이름 + +## Examples + +### 이벤트 이름을 변경한 경우 + +```tsx {8, 10} +function MyButton({ onButtonClick }: { onButtonClick?: () => void }) { + return + +``` + +### 디바운스 이벤트 + +```tsx + + + +``` + +### 스로틀 이벤트 + +```tsx + + + +``` diff --git a/docs/src/content/ko/docs/components/dom-event.mdx b/docs/src/content/ko/docs/components/dom-event.mdx new file mode 100644 index 0000000..cdf6ddd --- /dev/null +++ b/docs/src/content/ko/docs/components/dom-event.mdx @@ -0,0 +1,147 @@ +import { Callout } from "nextra/components"; + +# DOMEvent + +DOM 이벤트를 추적하는 데 사용됩니다. 자식 컴포넌트를 감싸고 지정된 이벤트 핸들러를 실행합니다. `createTracker`의 리턴 배열의 첫 번쨰 요소인 이벤트 컴포넌트 중 하나입니다. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ + DOMEvents: { + onFocus: (params, context) => { + // 포커스 이벤트 처리 + }, + }, +}); + +function App() { + return ( + + + + + + ); +} +``` + +## Reference + +### Props + +| Props | 타입 | 설명 | 필수 | +| ------------- | ----------------------------------------------------------------- | ------------------------------------------------------- | ---- | +| DOMEventNames | DOMEventNames | 이벤트 이름 (예: `"onClick"`, `"onFocus"`) | O | +| `enabled` | `boolean \| ((context: Context, params: EventParams) => boolean)` | 이벤트 추적을 조건부로 활성화/비활성화 (기본값: `true`) | - | +| `debounce` | `DebounceConfig` | 연속적인 이벤트 발생을 방지하는 디바운스 설정 | - | +| `throttle` | `ThrottleConfig` | 이벤트 발생 빈도를 제한하는 스로틀 설정 | - | +| `schema` | `string` | 이벤트 매개변수 검증을 위한 스키마 이름 | - | + + + 참고: `debounce`와 `throttle`은 상호 배타적이며 함께 사용할 수 없습니다. 타입 에러를 발생시켜 사전에 방지합니다. + + +스키마와 함께 사용하는 경우와 아닐 경우 타입이 달라지는 속성이 있습니다. + +스키마가 있을 경우: + +| Props | 타입 | 설명 | +| -------- | ---------------------------------------------------- | -------------------- | +| `params` | `SchemaParams \| (context: Context) => SchemaParams` | 스키마 기반 매개변수 | + +스키마가 없을 경우: + +| Props | 타입 | 설명 | +| -------- | -------------------------------------------------- | --------------- | +| `params` | `EventParams \| (context: Context) => EventParams` | 이벤트 매개변수 | + +--- + +#### `params` (필수) + +스키마와 함께 사용하는 경우: + +- 타입: `SchemaParams | (context: Context) => SchemaParams` +- 설명: 스키마 기반 매개변수 + +스키마와 함께 사용하지 않는 경우: + +- 타입: `EventParams | (context: Context) => EventParams` +- 설명: 이벤트 매개변수 + +#### `enabled` + +- 타입: `boolean | ((context: Context, params: EventParams) => boolean)` +- 설명: 이벤트 추적을 조건부로 활성화/비활성화 (기본값: `true`) + +#### `debounce` + +- 타입: `DebounceConfig` +- 설명: 연속적인 이벤트 발생을 방지하는 디바운스 설정 + +#### `throttle` + +- 타입: `ThrottleConfig` +- 설명: 이벤트 발생 빈도를 제한하는 스로틀 설정 + +#### `schema` + +스키마와 함께 사용하는 경우: + +- 타입: `string` +- 설명: 이벤트 매개변수 검증을 위한 스키마 이름 + +## Examples + +### 이벤트 이름을 변경한 경우 + +```tsx {8, 10} +function MyInput({ onInputFocus }: { onInputFocus?: () => void }) { + return ; +} + +function App() { + return ( + + + + + + ); +} +``` + +`eventName` 속성을 사용하면 사용자 정의 이벤트 핸들러 이름을 지정할 수 있습니다. 이 속성을 사용하면 핸들러 이름이 변경되어도 유연하게 대처할 수 있습니다. + +### 조건부 이벤트 + +```tsx + context.trackingEnabled}> + + +``` + +### 디바운스 이벤트 + +```tsx + + + +``` + +### 스로틀 이벤트 + +```tsx + +
스크롤 가능한 콘텐츠
+
+``` diff --git a/docs/src/content/ko/docs/components/impression.mdx b/docs/src/content/ko/docs/components/impression.mdx new file mode 100644 index 0000000..6f5f89f --- /dev/null +++ b/docs/src/content/ko/docs/components/impression.mdx @@ -0,0 +1,128 @@ +import { Callout } from "nextra/components"; + +# Impression + +[Intersection Observer API](https://developer.mozilla.org/docs/Web/API/Intersection_Observer_API)를 사용하여 노출 이벤트를 추적합니다. `createTracker`의 리턴 배열의 첫 번째 요소인 이벤트 컴포넌트 중 하나입니다. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ + impression: { + onImpression: (params, context) => { + // 노출 이벤트 처리 + }, + options: { + threshold: 0.5, + }, + }, +}); + +function App() { + return ( + + +
추적할 콘텐츠
+
+
+ ); +} +``` + +## Reference + +### Props + +| Props | 타입 | 설명 | 필수 | +| ---------- | ----------------------------------------------------------------- | ------------------------------------------------------- | ---- | +| `enabled` | `boolean \| ((context: Context, params: EventParams) => boolean)` | 이벤트 추적을 조건부로 활성화/비활성화 (기본값: `true`) | - | +| `debounce` | `DebounceConfig` | 연속적인 이벤트 발생을 방지하는 디바운스 설정 | - | +| `throttle` | `ThrottleConfig` | 이벤트 발생 빈도를 제한하는 스로틀 설정 | - | +| `options` | `ImpressionOptions` | `IntersectionObserver` 설정. 전역 옵션을 덮어씀 | - | +| `schema` | `string` | 이벤트 매개변수 검증을 위한 스키마 이름 | O | + + + 참고: `debounce`와 `throttle`은 상호 배타적이며 함께 사용할 수 없습니다. 타입 에러를 발생시켜 사전에 방지합니다. + + +스키마와 함께 사용하는 경우와 아닐 경우 타입이 달라지는 속성이 있습니다. + +스키마가 있을 경우: + +| Props | 타입 | 설명 | +| -------- | ---------------------------------------------------- | -------------------- | +| `params` | `SchemaParams \| (context: Context) => SchemaParams` | 스키마 기반 매개변수 | + +스키마가 없을 경우: + +| Props | 타입 | 설명 | +| -------- | -------------------------------------------------- | --------------- | +| `params` | `EventParams \| (context: Context) => EventParams` | 이벤트 매개변수 | + +--- + +#### `params` (필수) + +스키마와 함께 사용하는 경우: + +- 타입: `SchemaParams | (context: Context) => SchemaParams` +- 설명: 스키마 기반 매개변수 + +스키마와 함께 사용하지 않는 경우: + +- 타입: `EventParams | (context: Context) => EventParams` +- 설명: 이벤트 매개변수 + +#### `enabled` + +- 타입: `boolean | ((context: Context, params: EventParams) => boolean)` +- 설명: 이벤트 추적을 조건부로 활성화/비활성화 (기본값: `true`) + +#### `debounce` + +- 타입: `DebounceConfig` +- 설명: 연속적인 이벤트 발생을 방지하는 디바운스 설정 + +#### `throttle` + +- 타입: `ThrottleConfig` +- 설명: 이벤트 발생 빈도를 제한하는 스로틀 설정 + +#### `options` + +- 타입: `ImpressionOptions` +- 설명: [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) 동작을 제어하는 옵션입니다. 예: `threshold`, `rootMargin` 등 + 전역 설정을 덮어쓰며 개별 노출 이벤트에 대해 세부 설정할 수 있습니다. + +#### `schema` + +스키마와 함께 사용하는 경우: + +- 타입: `string` +- 설명: 이벤트 매개변수 검증을 위한 스키마 이름 + +## Examples + +### 조건부 이벤트 + +```tsx + context.user?.hasAccess} + options={{ threshold: 0.5 }} +> +
프리미엄 콘텐츠
+
+``` + +### 스로틀 이벤트 + +```tsx + +
광고 배너
+
+``` diff --git a/docs/src/content/ko/docs/components/index.mdx b/docs/src/content/ko/docs/components/index.mdx new file mode 100644 index 0000000..137c84b --- /dev/null +++ b/docs/src/content/ko/docs/components/index.mdx @@ -0,0 +1,63 @@ +# Provider + +`Provider` 컴포넌트는 애플리케이션에 초기 컨텍스트를 연결하고 제공합니다. `createTracker`의 리턴 배열의 첫 번쨰 요소인 이벤트 컴포넌트 중 하나입니다. + +```tsx +import { createTracker } from '@offlegacy/event-tracker' + +const [Track] = createTracker({...}) + +function App() { + return ( + + {/* 애플리케이션 */} + + ) +} +``` + +## Reference + +### Props + +| Props | 타입 | 설명 | 필수 | +| ---------------- | --------- | ---------------- | ---- | +| `initialContext` | `Context` | 초기 컨텍스트 값 | O | + +--- + +#### `initialContext` (필수) + +- 타입: `Context` +- 설명: 초기 컨텍스트 값 + +## Examples + +### 초기 컨텍스트를 설정한 경우 + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +type Context = { + userId: string; + trackingEnabled: boolean; +}; + +const [Track] = createTracker({}); + +function App() { + return ( + + + + ); +} +``` + +`initialContext`는 모든 하위 트래킹 컴포넌트에서 참조 가능한 초기 상태를 정의합니다. +이 컨텍스트는 `enabled`, `params`, 그리고 `SetContext`에서 사용될 수 있습니다. diff --git a/docs/src/content/ko/docs/components/page-view.mdx b/docs/src/content/ko/docs/components/page-view.mdx new file mode 100644 index 0000000..9b1c6c1 --- /dev/null +++ b/docs/src/content/ko/docs/components/page-view.mdx @@ -0,0 +1,107 @@ +import { Callout } from "nextra/components"; + +# PageView + +컴포넌트가 마운트될 때 페이지 뷰 이벤트를 추적합니다. `createTracker`의 리턴 배열의 첫 번째 요소인 이벤트 컴포넌트 중 하나입니다. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ + pageView: { + onPageView: (params, context) => { + // 페이지 뷰 이벤트 처리 + }, + }, +}); + +function App() { + return ( + + + + ); +} +``` + +## Reference + +### Props + +| Props | 타입 | 설명 | 필수 | +| ---------- | ----------------------------------------------------------------- | ------------------------------------------------------- | ---- | +| `enabled` | `boolean \| ((context: Context, params: EventParams) => boolean)` | 이벤트 추적을 조건부로 활성화/비활성화 (기본값: `true`) | - | +| `debounce` | `DebounceConfig` | 연속적인 이벤트 발생을 방지하는 디바운스 설정 | - | +| `throttle` | `ThrottleConfig` | 이벤트 발생 빈도를 제한하는 스로틀 설정 | - | +| `schema` | `string` | 이벤트 매개변수 검증을 위한 스키마 이름 | O | + + + 참고: `debounce`와 `throttle`은 상호 배타적이며 함께 사용할 수 없습니다. 타입 에러를 발생시켜 사전에 방지합니다. + + +스키마와 함께 사용하는 경우와 아닐 경우 타입이 달라지는 속성이 있습니다. + +스키마가 있을 경우: + +| Props | 타입 | 설명 | +| -------- | ---------------------------------------------------- | -------------------- | +| `params` | `SchemaParams \| (context: Context) => SchemaParams` | 스키마 기반 매개변수 | + +스키마가 없을 경우: + +| Props | 타입 | 설명 | +| -------- | -------------------------------------------------- | --------------- | +| `params` | `EventParams \| (context: Context) => EventParams` | 이벤트 매개변수 | + +--- + +#### `params` (필수) + +스키마와 함께 사용하는 경우: + +- 타입: `SchemaParams | (context: Context) => SchemaParams` +- 설명: 스키마 기반 매개변수 + +스키마와 함께 사용하지 않는 경우: + +- 타입: `EventParams | (context: Context) => EventParams` +- 설명: 이벤트 매개변수 + +#### `enabled` + +- 타입: `boolean | ((context: Context, params: EventParams) => boolean)` +- 설명: 이벤트 추적을 조건부로 활성화/비활성화 (기본값: `true`) + +#### `debounce` + +- 타입: `DebounceConfig` +- 설명: 연속적인 이벤트 발생을 방지하는 디바운스 설정 + +#### `throttle` + +- 타입: `ThrottleConfig` +- 설명: 이벤트 발생 빈도를 제한하는 스로틀 설정 + +#### `schema` + +스키마와 함께 사용하는 경우: + +- 타입: `string` +- 설명: 이벤트 매개변수 검증을 위한 스키마 이름 + +## Examples + +### 조건부 이벤트 + +```tsx + context.trackingConsent === true} +/> +``` + +### 디바운스 페이지 뷰 + +```tsx + +``` diff --git a/docs/src/content/ko/docs/components/set-context.mdx b/docs/src/content/ko/docs/components/set-context.mdx new file mode 100644 index 0000000..2e89ee7 --- /dev/null +++ b/docs/src/content/ko/docs/components/set-context.mdx @@ -0,0 +1,48 @@ +import { Callout } from "nextra/components"; + +# SetContext + +트래킹 컨텍스트를 설정하거나 업데이트합니다. `createTracker`의 리턴 배열의 첫 번째 요소인 유틸리티 컴포넌트 중 하나입니다. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +const [Track] = createTracker({ ... }); + +function App() { + return ( + + + + ); +} +``` + +## Reference + +### Props + +| Props | 타입 | 설명 | 필수 | +| --------- | ------------------------------------------------ | ------------------------------------------------------------ | ---- | +| `context` | `Context \| ((prevContext: Context) => Context)` | 새로운 컨텍스트 값 또는 기존 값을 기반으로 업데이트하는 함수 | O | + +--- + +#### `context` (필수) + +- 타입: `Context | ((prevContext: Context) => Context)` +- 설명: 새 컨텍스트 값을 직접 지정하거나, 이전 컨텍스트 값을 기반으로 업데이트할 수 있습니다. 이 컴포넌트는 한 번 렌더링되면 지정된 컨텍스트로 추적 컨텍스트가 갱신됩니다. + +## Examples + +### 정적 컨텍스트 설정 + +```tsx + +``` + +### 이전 컨텍스트 기반 업데이트 + +```tsx + ({ ...prev, lastSeen: new Date() })} /> +``` diff --git a/docs/src/content/ko/docs/create-tracker.mdx b/docs/src/content/ko/docs/create-tracker.mdx new file mode 100644 index 0000000..6a540b8 --- /dev/null +++ b/docs/src/content/ko/docs/create-tracker.mdx @@ -0,0 +1,127 @@ +# createTracker + +`createTracker`는 사용자 정의 설정으로 트래커 인스턴스를 생성하는 함수입니다. 이 함수의 파라미터를 통해 이벤트 트래킹을 정의하고, 반환된 튜플을 통해 트래커 인스턴스를 사용할 수 있습니다. + +```tsx +const [{ Provider, DOMEvent, Click, Impression, PageView, SetContext }, useTracker] = createTracker({ + init, + send, + DOMEvents, + impression: { + onImpression, + options, + }, + pageView: { + onPageView, + }, + batch, + schemas, +}); +``` + +## Reference + +### Parameters + +`createTracker`는 하나의 객체를 인자로 받습니다. + +| 속성 (Property) | 타입 | 설명 | +| --------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | +| `init` | `(initialContext: Context, setContext: SetContext) => void \| Promise` | 모든 이벤트 발생 전에 실행되는 초기화 함수. (비동기 지원) | +| `send` | `(params: EventParams \| (context: Context) => EventParams, context: Context, setContext: SetContext) => TaskReturnType` | 이벤트 전송 함수. (비동기 지원) | +| `DOMEvents` | [`DOMEvents`](https://developer.mozilla.org/docs/Web/API/Event) | React DOM 이벤트 핸들러 모음 (`onClick`, `onMouseEnter` 등) | +| `impression` | `ImpressionOptions` | 노출 이벤트 트래킹 설정 | +| `pageView` | `PageViewOptions` | 페이지 뷰 트래킹 설정 | +| `batch` | `BatchConfig` | 이벤트 배칭 설정 | +| `schemas` | `SchemaConfig` | 이벤트 스키마 검증 설정 | + +--- + +#### `init` + +- 타입: `(initialContext: Context, setContext: SetContext) => void | Promise` +- 설명: 모든 이벤트가 발생하기 전, 초기 컨텍스트를 설정하는 함수입니다. (비동기를 지원합니다.) + +#### `send` + +- 타입: `(params: EventParams | (context: Context) => EventParams, context: Context, setContext: SetContext) => TaskReturnType` +- 설명: 이벤트를 외부로 전송하는 함수입니다. (비동기를 지원합니다.) + +#### `DOMEvents` + +- 타입: `DOMEvents` +- 설명: 표준 React DOM 이벤트 핸들러 객체입니다. `` 컴포넌트에서 사용됩니다. + +#### `impression` + +- `impression.onImpression` + + - 타입: `(params: EventParams | (context: Context) => EventParams, context: Context, setContext: SetContext) => TaskReturnType` + - 설명: `` 발생 시 실행되는 콜백 함수입니다. + +- `impression.options` + + - 타입: `ImpressionOptions` + - 속성: + + - `threshold`: 노출 비율 기준 (기본값: `0.2`) + - `freezeOnceVisible`: 노출 후 상태 고정 여부 (기본값: `true`) + - `initialIsIntersecting`: 초기 교차 여부 (기본값: `false`) + +#### `pageView` + +- `pageView.onPageView` + + - 타입: `(params: EventParams | (context: Context) => EventParams, context: Context, setContext: SetContext) => TaskReturnType` + - 설명: `` 컴포넌트가 마운트 시 실행됩니다. + +#### `batch` + +- 타입: `BatchConfig` +- 속성: + + - `batch.enable`: 배치 활성화 여부 (기본값: `false`) + - `batch.interval`: 전송 간격(ms, 기본값: `3000`) + - `batch.thresholdSize`: 최대 배치 크기 (기본값: `25`) + - `batch.onFlush`: 이벤트 전송 처리 함수 + - `batch.onError`: 오류 발생 시 처리 함수 (선택) + +#### `schemas` + +- 타입: `SchemaConfig` +- 속성: + + - `schemas.schema`: [Zod](https://zod.dev/) 기반의 스키마 정의 + - `schemas.onSchemaError`: 스키마 오류 발생 시 처리 함수 + - `schemas.abortOnError`: 오류 시 이벤트 중단 여부 (기본값: `false`) + +### Returns + +`createTracker`는 다음 두 값을 포함한 튜플을 반환합니다. + +```tsx +const [{ Provider, DOMEvent, Click, Impression, PageView, SetContext }, useTracker] = createTracker({...}) +``` + +#### [Components](./components) + +리턴 배열에서 첫 요소인 이벤트 컴포넌트는 여러 가지 이벤트 컴포넌트를 포함하고 있습니다. + +```tsx +const [{ Provider, DOMEvent, Click, Impression, PageView, SetContext }] = createTracker(config); +``` + +- `Provider` +- `DOMEvent` +- `Click` +- `Impression` +- `PageView` +- `SetContext` + +#### [Hook](./hook) + +```tsx +const [, useTracker] = createTracker(config); +``` + +리턴 배열에서 두번째 요소인 커스텀 React hook입니다. 이 훅은 컴포넌트 내에서 이벤트 트래킹 기능과 컨텍스트 관리에 접근할 수 있게 합니다. diff --git a/docs/src/content/ko/docs/data-type-validation.mdx b/docs/src/content/ko/docs/data-type-validation.mdx new file mode 100644 index 0000000..9f9a2af --- /dev/null +++ b/docs/src/content/ko/docs/data-type-validation.mdx @@ -0,0 +1,96 @@ +import { Callout, Steps } from "nextra/components"; + +# 데이터 검증 + +Event Tracker는 이벤트 파라미터의 정확성을 보장하기 위해 [Zod](https://zod.dev/)를 활용한 스키마 기반 데이터 타입 검증 기능을 제공합니다. +이 기능을 사용하면 이벤트가 수집되기 전에 사전 정의된 구조와 일치하는지 검증할 수 있으며, 이는 데이터 신뢰성과 오류 방지, 정형 분석을 위한 기반으로 유용합니다. + +## Usage + +Zod 스키마를 정의하고, `createTracker` 설정에서 `schemas` 옵션으로 전달하면 됩니다. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; +import { z } from "zod"; + +const schemas = { + page_view: z.object({ + title: z.string(), + }), + click_button: z.object({ + target: z.string(), + }), +}; + +const [Track] = createTracker<{}, {}, typeof schemas>({ + schemas: { + schemas, + onSchemaError: (error) => { + console.error("스키마 검증 실패:", error); + }, + abortOnError: true, + }, +}); +``` + +## Reference + +| 옵션 | 타입 | 설명 | 기본값 | +| --------------- | -------------------------------------------- | ---------------------------------------- | ------- | +| `schemas` | `Record` | 이벤트 이름과 Zod 스키마의 매핑 | 필수 | +| `onSchemaError` | `(error: ZodError) => void \| Promise` | 스키마 검증 실패 시 호출되는 함수 | 선택 | +| `abortOnError` | `boolean` | 오류 발생 시 이벤트 처리를 중단할지 여부 | `false` | + +스키마 오류가 발생했을 때 `abortOnError: true`인 경우, 해당 이벤트는 추적되지 않습니다. + +## Examples + + + +### Zod 스키마 정의 + +```ts +import { z } from "zod"; + +const schemas = { + page_view: z.object({ + title: z.string(), + }), + click_button: z.object({ + target: z.string(), + }), +}; +``` + +### `createTracker`에 등록 + +```tsx +const [Track] = createTracker<{}, {}, typeof schemas>({ + schemas: { + schemas, + abortOnError: true, + onSchemaError: (error) => { + console.error("스키마 오류 발생", error); + }, + }, +}); +``` + +### 스키마 기반 이벤트 트래킹 + +```tsx + + +``` + +스키마 검증이 성공하면 이벤트는 `send` 또는 각 핸들러로 전달됩니다. +실패하면 `onSchemaError`가 호출되고, `abortOnError`가 true일 경우 이벤트는 무시됩니다. + + + +## Notes + +- Zod 스키마는 기본적으로 [parse](https://zod.dev/?id=parse)를 사용하여 검증됩니다. +- 부드러운 타입 추론을 위해, createTracker 함수 외부에서 스키마를 정의하고 createTracker의 세 번째 타입 인자에 typeof schemas를 할당하길 권장합니다. + +_향후 다른 스키마 검증 라이브러리를 지원할 계획입니다._ diff --git a/docs/src/content/ko/hook.mdx b/docs/src/content/ko/docs/hook.mdx similarity index 50% rename from docs/src/content/ko/hook.mdx rename to docs/src/content/ko/docs/hook.mdx index c775e8f..09d6258 100644 --- a/docs/src/content/ko/hook.mdx +++ b/docs/src/content/ko/docs/hook.mdx @@ -1,31 +1,37 @@ -# hook +import { Callout } from "nextra/components"; -[`createTracker`](/docs/create-tracker)에서 두 번째 배열 항목으로 반환되는 커스텀 React hook입니다. -이 훅은 컴포넌트 내에서 이벤트 트래킹 기능과 컨텍스트 관리에 접근할 수 있게 합니다. +# Hook + +React 컴포넌트 내부에서 트래킹 컨텍스트를 설정하거나, 명령형 방식으로 이벤트를 추적할 수 있습니다. 해당 Hook은 [`createTracker`](/docs/create-tracker)에서 반환되는 두 번째 항목입니다. + +주로 컴포넌트 마운트 이후, 사용자 입력이나 비동기 작업이 완료된 시점에서 수동으로 이벤트를 트리거할 때 사용됩니다. ```tsx import { createTracker } from "@offlegacy/event-tracker"; -const [Track, useTracker] = createTracker({...}) +const [Track, useTracker] = createTracker({...}); function MyComponent() { const { setContext, getContext, track, trackWithSchema } = useTracker(); - return ( - // 컴포넌트 내용 - ); + // 명령형 이벤트 트래킹 } ``` -### 반환 값 +## Reference -이 hook은 다음 속성을 포함하는 객체를 반환합니다: +### Returns -#### setContext +| 속성 | 타입 | 설명 | +| ----------------- | --------------------------------------------------------------- | ------------------------------------------- | +| `setContext` | `(context: Context \| (prev: Context) => Context) => void` | 트래킹 컨텍스트 설정 또는 업데이트 | +| `getContext` | `() => Context` | 현재 트래킹 컨텍스트 반환 | +| `track` | `Record void>` | 일반 이벤트 추적 함수 집합 | +| `trackWithSchema` | `Record void>` | 스키마 기반 검증 포함 이벤트 추적 함수 집합 | -- Type: `(context: Context) => void` -- 현재 트래킹 컨텍스트를 설정하거나 업데이트합니다. -- 사용자 정보, 세션 데이터 등을 업데이트하는 데 사용할 수 있습니다. +--- + +#### `setContext` ```tsx const { setContext } = useTracker(); @@ -40,11 +46,10 @@ setContext((prev) => ({ })); ``` -#### getContext +- 현재 트래킹 컨텍스트를 설정하거나 업데이트합니다. +- 사용자 정보, 세션 데이터 등을 업데이트하는 데 사용할 수 있습니다. -- Type: `() => Context` -- 현재 트래킹 컨텍스트를 반환합니다. -- 현재 트래킹 상태에 접근하는 데 유용합니다. +#### `getContext` ```tsx const { getContext } = useTracker(); @@ -53,12 +58,10 @@ const currentContext = getContext(); console.log("Current user:", currentContext.userId); ``` -#### track +- 현재 트래킹 컨텍스트를 반환합니다. +- 현재 트래킹 상태에 접근하는 데 유용합니다. -- Type: `Record void>` -- 모든 구성된 이벤트 트래킹 함수를 포함하는 객체 -- key는 트래커 구성에 정의된 이벤트 이름과 일치합니다. -- 고급 제어를 위한 선택적 `TrackingOptions`를 허용합니다. +#### `track` ```tsx const { track } = useTracker(); @@ -94,12 +97,11 @@ track.onClick( track.onImpression({ elementId: "hero" }); ``` -#### trackWithSchema - -- Type: `Record void>` -- 스키마 검증이 포함된 모든 구성된 이벤트 트래킹 함수를 포함하는 객체 +- 모든 구성된 이벤트 트래킹 함수를 포함하는 객체 - key는 트래커 구성에 정의된 이벤트 이름과 일치합니다. -- 고급 제어를 위한 선택적 `TrackingOptions`를 허용합니다. +- 고급 제어를 위한 선택적 TrackingOptions를 허용합니다. + +#### `trackWithSchema` ```tsx const { trackWithSchema } = useTracker(); @@ -130,39 +132,64 @@ trackWithSchema.onImpression( ); ``` -### TrackingOptions +- 스키마 검증이 포함된 모든 구성된 이벤트 트래킹 함수를 포함하는 객체 +- key는 트래커 구성에 정의된 이벤트 이름과 일치합니다. +- 고급 제어를 위한 선택적 TrackingOptions를 허용합니다. -`track`과 `trackWithSchema` 메서드 모두 다음 옵션을 포함하는 선택적 두 번째 매개변수를 허용합니다: +## TrackingOptions -- `enabled?: boolean | ((context: Context, params: EventParams) => boolean)` - 이벤트 추적을 조건부로 활성화/비활성화 -- `debounce?: DebounceConfig` - 연속적인 이벤트 발생을 방지하는 디바운스 설정 -- `throttle?: ThrottleConfig` - 이벤트 발생 빈도를 제한하는 스로틀 설정 +`track`, `trackWithSchema` 함수는 모두 아래 옵션을 선택적으로 받을 수 있습니다: -**참고:** `debounce`와 `throttle`은 상호 배타적이며 함께 사용할 수 없습니다. +| 옵션 | 타입 | 설명 | +| ---------- | ------------------------------------------- | ------------------------------------- | +| `enabled` | `boolean \| ((context, params) => boolean)` | 조건부로 이벤트 추적 활성화 여부 설정 | +| `debounce` | `DebounceConfig` | 연속된 이벤트를 제한 (후행 호출 1회) | +| `throttle` | `ThrottleConfig` | 짧은 시간 내 중복 호출 방지 | -#### DebounceConfig + + 참고: `debounce`와 `throttle`은 상호 배타적이며 함께 사용할 수 없습니다. 타입 에러를 발생시켜 사전에 방지합니다. + -```tsx +#### `DebounceConfig` + +```ts interface DebounceConfig { - delay: number; // 밀리초 단위의 지연 시간 - leading?: boolean; // 선행 에지에서 실행 (기본값: false) - trailing?: boolean; // 후행 에지에서 실행 (기본값: true) + delay: number; + leading?: boolean; // 기본값: false + trailing?: boolean; // 기본값: true } ``` -#### ThrottleConfig +#### `ThrottleConfig` -```tsx +```ts interface ThrottleConfig { - delay: number; // 밀리초 단위의 지연 시간 - leading?: boolean; // 선행 에지에서 실행 (기본값: true) - trailing?: boolean; // 후행 에지에서 실행 (기본값: false) + delay: number; + leading?: boolean; // 기본값: true + trailing?: boolean; // 기본값: false } ``` -### 사용 예제 +## Best Practices + +### 컨텍스트 관리 + +- 여러 이벤트에 영향을 주는 전역 상태를 업데이트하기 위해 `setContext`를 사용하세요. +- 이전 상태에 기반한 업데이트를 위해 `setContext`의 함수 형태를 고려하세요. + +### 이벤트 트래킹 + +- 가능한 경우 `track` 또는 `trackWithSchema`에서 이벤트 함수를 사용하세요. + +### 성능 최적화 -다음은 이 hook을 사용하는 예제입니다: +- 렌더링 중에 이벤트 트래킹 함수를 호출하지 마세요. +- [배칭](/docs/batching)을 사용하여 성능을 향상시키세요. +- [데이터 검증](/docs/data-type-validation)을 사용하여 데이터 안전성을 확인하세요. + +## Examples + +### 기본 ```tsx import { createTracker } from "@offlegacy/event-tracker"; @@ -204,21 +231,3 @@ function UserProfile({ userId }) { ); } ``` - -### Best Practices - -1. **컨텍스트 업데이트** - - - 여러 이벤트에 영향을 주는 전역 상태를 업데이트하기 위해 `setContext`를 사용하세요. - - 이전 상태에 기반한 업데이트를 위해 `setContext`의 함수 형태를 고려하세요. - -2. **이벤트 트래킹** - - - 가능한 경우 `track` 또는 `trackWithSchema`에서 이벤트 함수를 사용하세요. - -3. **성능** - - - 렌더링 중에 이벤트 트래킹 함수를 호출하지 마세요. - - 콜백 또는 효과를 사용하세요. - - [배칭](/docs/batching)을 사용하여 성능을 향상시키세요. - - [데이터 타입 검증](/docs/data-type-validation)을 사용하여 데이터 타입 안전성을 확인하세요. diff --git a/docs/src/content/ko/docs/installation.mdx b/docs/src/content/ko/docs/installation.mdx new file mode 100644 index 0000000..85fd943 --- /dev/null +++ b/docs/src/content/ko/docs/installation.mdx @@ -0,0 +1,11 @@ +# 설치 + +Event Tracker는 React 애플리케이션에서 이벤트 트래킹을 쉽게 구현할 수 있도록 설계된 라이브러리입니다. `React@^16.8.0` 이상의 버전에서 사용할 수 있습니다. + +최신 안정 버전을 설치하려면 아래 명령어를 실행하세요. + +![NPM Version](https://img.shields.io/npm/v/%40offlegacy%2Fevent-tracker) + +```shell npm2yarn +npm install @offlegacy/event-tracker +``` diff --git a/docs/src/content/ko/docs/introduction.mdx b/docs/src/content/ko/docs/introduction.mdx new file mode 100644 index 0000000..7517a5b --- /dev/null +++ b/docs/src/content/ko/docs/introduction.mdx @@ -0,0 +1,83 @@ +# 소개 + +Event Tracker 문서에 오신 것을 환영합니다. + +## Event Tracker가 무엇인가요? + +Event Tracker는 복잡한 이벤트 트래킹 구현 과정을 단순화하고, 개발자가 비즈니스 로직에 더 집중할 수 있도록 돕는 선언적 방식의 React 라이브러리입니다. 모든 규모의 애플리케이션에서 이벤트 트래킹을 쉽고 효율적으로 관리할 수 있도록 설계되었습니다. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; + +// 트래커 인스턴스 생성 +const [Track, useTracker] = createTracker({ + DOMEvents: { + onClick: (params, context) => { + log("Click event:", params, context); + }, + }, +}); + +// 앱에서 사용하기 +function App() { + return ( + + + + + + ); +} +``` + +### 주요 기능 + +Event Tracker는 개발자 경험과 애플리케이션 성능을 모두 고려한 다양한 기능을 제공합니다. + +| Feature | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 선언적 API | 트래킹을 위한 별도의 설정이나 복잡한 코드 없이, 컴포넌트 형태로 직관적으로 선언할 수 있습니다. | +| 애널리틱스 도구 독립성 | [Google Analytics](https://analytics.google.com/), [Amplitude](https://amplitude.com/) 등 어떤 도구든 자유롭게 연동할 수 있어, 기존 인프라를 변경하지 않고도 도입이 가능합니다. | +| 명확한 관심사 분리 | 트래킹 로직을 비즈니스 코드에서 완전히 분리함으로써 코드의 가독성, 테스트 용이성, 유지보수성을 모두 향상시킵니다. | +| 최적화된 성능 | 배칭, 디바운스, 스로틀링 등 네트워크 요청을 최소화하는 다양한 전략이 내장되어 있어, 성능 저하 없이 안정적으로 트래킹할 수 있습니다. | +| 실행 순서 보장 | 비동기 상황에서도 이벤트가 의도한 순서대로 처리되도록 설계되어, 복잡한 사용자 흐름에서도 정확한 트래킹이 가능합니다. | +| 데이터 검증 | [Zod](https://zod.dev/) 기반의 정적 스키마 검증으로 빌드 타임에서 오류를 방지하고, 수집되는 이벤트 데이터의 신뢰도를 보장합니다. | + +## 핵심 개념 + +Event Tracker를 효과적으로 사용하기 위해 알아야 할 몇 가지 핵심 개념이 있습니다. + +### 인스턴스 (`createTracker`) + +라이브러리의 가장 기본적인 출발점입니다. `createTracker` 함수를 사용하여 트래커 인스턴스(`Track` 컴포넌트 컬렉션과 `useTracker` 훅)를 생성합니다. 이때, DOM 이벤트 핸들러, 노출(Impression) 이벤트 핸들러, 스키마 등을 설정하여 이벤트 트래킹을 정의합니다. + +목적에 따라 구분하여 여러 가지 트래커 인스턴스를 생성할 수 있습니다. (예를 들어, Google Analytics로 보내는 이벤트와 Amplitude로 보내는 이벤트를 구분하여 생성할 수 있습니다.) + +### 프로바이더 (`Track.Provider`) + +React의 Context API를 기반으로 구현되었습니다. 애플리케이션 또는 특정 컴포넌트 트리의 최상단에서 `Track.Provider`로 감싸 하위 컴포넌트들에 트래킹에 필요한 공통 데이터(컨텍스트)를 제공합니다. 예를 들어, `userId`, `pageName` 등의 정보를 컨텍스트로 전달하면, 각 이벤트 트래킹 시 이 정보를 활용할 수 있습니다. + +### 이벤트 컴포넌트 (`Track.Click`, `Track.PageView` 등) + +선언적으로 이벤트를 트래킹할 수 있도록 제공되는 특수 컴포넌트들입니다. `createTracker` 리턴 배열의 첫 번째 요소입니다. + +- `Track.Click`: 자식 요소에서 클릭 이벤트가 발생했을 때 트래킹합니다. +- `Track.Impression`: 자식 요소가 화면에 노출되었을 때 트래킹합니다. +- `Track.PageView`: 컴포넌트가 마운트될 때 페이지 뷰 이벤트를 트래킹합니다. + +이 외에도 다양한 사용자 인터랙션 및 생명주기 이벤트에 대응하는 컴포넌트를 제공하거나 커스텀하여 사용할 수 있습니다. 각 컴포넌트는 `context`, `params` prop을 통해 해당 이벤트와 관련된 특정 데이터를 전달받고 활용할 수 있습니다. + +### 커스텀 훅 + +컴포넌트의 생명주기나 DOM 이벤트와 직접적으로 관련되지 않은, 보다 복잡하거나 조건부적인 이벤트 트래킹이 필요할 때 사용합니다. 훅을 사용하면 `Track.Provider`로부터 컨텍스트 정보를 가져오고, 정의된 트래킹 로직을 명령형으로 사용할 수 있습니다. + +## 다음 단계 + +이러한 핵심 개념들은 서로 유기적으로 작동하여 Event Tracker의 강력하고 유연한 이벤트 트래킹 환경을 구성합니다. 더 자세한 사용법과 각 기능에 대한 심층적인 내용은 아래 문서들에서 확인하실 수 있습니다. + +- [왜 Event Tracker인가요?](/docs/why-event-tracker): Event Tracker의 필요성과 주요 기능 알아보기 +- [`createTracker`](/docs/create-tracker): 트래커 인스턴스 생성 및 설정 방법 알아보기 +- [Components](/docs/components): 사용 가능한 모든 트래킹 컴포넌트와 사용 예시 살펴보기 +- [Hook](/docs/hook): 커스텀 훅을 활용한 사용자 지정 트래킹 방법 알아보기 +- [배칭](/docs/batching): 이벤트 배칭을 통한 성능 최적화 전략 알아보기 +- [데이터 검증](/docs/data-type-validation): Zod 스키마를 활용한 데이터 유효성 검증하기 diff --git a/docs/src/content/ko/docs/why-event-tracker.mdx b/docs/src/content/ko/docs/why-event-tracker.mdx new file mode 100644 index 0000000..5a504aa --- /dev/null +++ b/docs/src/content/ko/docs/why-event-tracker.mdx @@ -0,0 +1,198 @@ +import { Steps } from "nextra/components"; + +# 왜 Event Tracker인가요? + +현대 웹 애플리케이션은 사용자의 행동을 분석하여 서비스 품질을 지속적으로 개선해야 합니다. 그러나 기존의 이벤트 트래킹 방식은 여러 문제점이 나타납니다. + +## Event Tracker가 필요한 이유 + +다음은 전통적인 이벤트 트래킹 방식의 문제점을 보여주는 예시입니다. + +```tsx {8,15, 24-31} +function Page() { + const { user, userId } = useUser(); // 사용자 정보와 ID를 가져옵니다. + + return ( +
+

User: {user.name}

+ {/* Counter 컴포넌트에 이벤트 트래킹을 위해 userId를 전달합니다. */} + +
+ ); +} + +// 오직 이벤트 트래킹만을 위해서 'userId'를 prop으로 전달받습니다. +// 만약 Counter가 더 깊은 곳에 있다면, prop drilling은 더욱 심해집니다. +function Counter({ userId }: { userId: string }) { + const [count, setCount] = useState(0); + const { track } = useTrackEvent(); // 가상의 트래킹 훅 + + const handleIncrement = () => { + const newCount = count + 1; + setCount(newCount); + + // 비즈니스 로직 (카운트 증가)과 트래킹 로직이 혼재합니다. + track({ + event: "click_increment", + params: { + type: "count", + value: newCount, + userId, // 상위로부터 전달받은 userId 사용 + }, + }); + }; + + return ( +
+

Count: {count}

+ +
+ ); +} +``` + + + +### Prop Drilling의 고통 + +이벤트 트래킹에 필요한 데이터를 하위 컴포넌트까지 전달하기 위해 수많은 계층을 거쳐 prop을 내려보내야 하는 경우가 많습니다. 이는 코드의 가독성을 해치고 유지보수를 어렵게 만듭니다. + +### 로직의 강한 결합 + +비즈니스 로직과 이벤트 트래킹 로직이 한데 섞여 코드의 복잡도를 높이고, 각 로직의 독립적인 테스트와 수정을 어렵게 만듭니다. + +### 보일러플레이트 코드 증가 + +반복적인 트래킹 코드 작성은 개발 생산성을 저해하는 요인이 됩니다. + + + +## Event Tracker가 제시하는 새로운 패러다임 + +Event Tracker는 이벤트 트래킹을 위한 새로운 패러다임을 소개합니다. +Event Tracker가 제시하는 선언적 방식은 전통적으로 이벤트 트래킹과 관련된 복잡성을 단순화하여, 모든 개발자들이 쉽게 접근할 수 있도록 합니다. + + + +### 선언적 이벤트 트래킹 + +```tsx {7, 10, 12, 32, 34} +function Page() { + const { user, userId } = useUser(); + + // Track.Provider를 통해 하위 컴포넌트에 트래킹 컨텍스트(userId)를 제공합니다. + // 더 이상 prop drilling이 필요 없습니다. + return ( + +
+

User: {user.name}

+ {/* userId를 prop으로 전달할 필요가 없습니다. */} +
+
+ ); +} + +function Counter() { + const [count, setCount] = useState(0); + + const handleIncrement = () => { + // 이제 handleIncrement 함수는 순수하게 카운트 증가 로직만 담당합니다. + setCount(count + 1); + }; + + return ( +
+

Count: {count}

+ {/* + Track.Click 컴포넌트가 클릭 이벤트를 감싸고, + 클릭 발생 시 정의된 파라미터와 함께 이벤트를 트래킹합니다. + 컨텍스트로 제공된 userId는 자동으로 트래킹 데이터에 포함됩니다. + */} + + + +
+ ); +} +``` + +Event Tracker를 사용하면 선언적 이벤트 트래킹이 가능해져 코드 가독성이 향상되고 복잡성이 감소합니다. 이는 개발자들이 이벤트 트래킹을 더 쉽게 이해하고 사용할 수 있도록 돕습니다. + +이제 `handleIncrement` 함수는 카운트 증가에만 책임이 있고, 이벤트 트래킹은 `` 컴포넌트가 처리합니다. +**이러한 선언적 접근 방식은 개발자가 '어떻게 트래킹할지'가 아닌 '무엇을 트래킹할지'에 집중하도록 합니다.** +어떻게 트래킹할지는 React 앱 외부에서 정의되어야 합니다. + +### 이벤트 트래킹 응집도 개선 + +```tsx {4-10, 14-20} +const [Track, useTracker] = createTracker({ + // DOM 이벤트 발생 시 실행될 콜백 함수 + DOMEvents: { + onClick: (params, context) => { + // 실제 트래킹 라이브러리(Google Analytics, Amplitude 등) 호출 + logEvent("click_event", { + ...params, // { value: ..., type: "count" } + userId: context.userId, // Provider로부터 받은 userId + }); + }, + // 필요에 따라 onMouseOver, onFocus 등 다양한 DOM 이벤트 핸들러 정의 가능 + }, + // 화면 노출(Impression) 이벤트 발생 시 실행될 콜백 함수 + onImpression: (params, context) => { + logEvent("impression_event", { + ...params, + userId: context.userId, + pagePath: window.location.pathname, + }); + }, +}); +``` + +이제 '어떻게 추적할지'에 대한 코드가 비즈니스 로직과 분리되었습니다. 애플리케이션 외부에 위치하므로 비즈니스 로직을 변경하지 않고도 이벤트 트래킹 로직을 수정할 수 있습니다. + +### 데이터 검증 + +```tsx {13-20, 24-33} +import { z } from "zod"; +import { createTracker } from "@offlegacy/Event Tracker"; + +interface Context { + /* ... */ +} + +interface Params { + /* ... */ +} + +// 스키마 정의 +const schemas = { + page_view: z.object({ + title: z.string(), + }), + click_button: z.object({ + target: z.string(), + }), +}; + +// 트래커 설정 +const [Track] = createTracker({ + schema: { + schemas: { + page_view, + click_button, + }, + onSchemaError: (error) => { + console.error("Schema validation error:", error); + }, + abortOnError: true, + }, +}); + +// 스키마 사용하기 +; +; +``` + +Event Tracker는 [Zod](https://zod.dev/) 라이브러리와 통합하여 스키마 기반의 강력한 데이터 타입 검증 기능을 제공합니다. 이를 통해 개발 단계에서부터 데이터 오류를 방지하고, 트래킹 데이터의 신뢰성을 높일 수 있습니다. + +
diff --git a/docs/src/content/ko/docs/with-google-analytics-example.mdx b/docs/src/content/ko/docs/with-google-analytics-example.mdx new file mode 100644 index 0000000..c0e1eae --- /dev/null +++ b/docs/src/content/ko/docs/with-google-analytics-example.mdx @@ -0,0 +1,115 @@ +import { Steps } from "nextra/components"; + +# Google Analytics 연동하기 + +GA4(Google Analytics 4)와 쉽게 통합해 사용자 행동을 추적하는 방법을 단계별로 안내합니다. + + + +## GA4 스크립트 설정 + +`index.html` ``에 GA4 스크립트를 추가하여 `gtag`를 사용할 준비를 합니다. + +```html + + + + + +``` + +## `sendGA4` 함수 정의하기 + +`gtag` 호출 전 확인 및 경고 로직을 포함한 전송 함수를 정의합니다. + +```ts +export function sendGA4(eventName: string, eventParams: Record = {}): void { + if (typeof window === "undefined") return; + if (typeof window.gtag !== "function") { + console.warn("[GA4] gtag 함수가 로드되지 않았습니다."); + return; + } + window.gtag("event", eventName, eventParams); +} +``` + +## 트래킹 이벤트 정의하기 + +`createTracker`를 통해 GA4 전송 로직과 DOM/노출/페이지뷰 이벤트를 설정합니다. + +```tsx +import { createTracker } from "@offlegacy/event-tracker"; +import { sendGA4 } from "./sendGA4"; + +interface GA4Context { + userId: string; +} + +interface GA4EventParams { + eventName: string; +} + +export const [Track, useTracker] = createTracker({ + DOMEvents: { + onClick: (params, ctx) => sendGA4(params.eventName, { ...params, user_id: ctx.userId }), + }, + impression: { + onImpression: (params, ctx) => sendGA4("impression", { ...params, user_id: ctx.userId }), + options: { threshold: 0.5, freezeOnceVisible: true }, + }, +}); +``` + +## 블로그 페이지에서 사용하기 + +`Track.Provider`로 컨텍스트를 감싸고, ``, ``, `` 컴포넌트를 사용해 이벤트를 선언적으로 기록합니다. + +```tsx +import { Track } from "./event-tracker"; + +function App() { + return ( + + + + ); +} + +interface PostProps { + id: number; + title: string; + slug: string; + content: string; +} + +function Post({ post }: PostProps) { + return ( +
+

{post.title}

+ + +
{post.content}
+
+ +
+ + + + + + + +
+
+ ); +} +``` + +
diff --git a/docs/src/content/ko/docs/with-zod-example.mdx b/docs/src/content/ko/docs/with-zod-example.mdx new file mode 100644 index 0000000..53542ee --- /dev/null +++ b/docs/src/content/ko/docs/with-zod-example.mdx @@ -0,0 +1,72 @@ +import { Steps } from "nextra/components"; + +# Zod와 함께 사용하기 + +[Zod](https://zod.dev/)와 함께 사용하여 강력한 데이터 타입 검증을 구현하는 방법을 알아보겠습니다. +Zod를 통한 스키마 기반 검증으로 런타임 이전에 오류를 방지하고 데이터 신뢰성을 보장할 수 있습니다. + + + +## 스키마 정의하기 + +이벤트별 필드와 타입을 Zod 객체로 선언합니다. + +```tsx {4-5} +import { z } from "zod"; + +export const schemas = { + page_view: z.object({ title: z.string() }), + click_button: z.object({ target: z.string() }), +}; +``` + +## 트래킹 이벤트 정의하기 + +스키마를 createTracker에 연결해 이벤트 파라미터를 자동 검증합니다. + +```tsx {5-9} +import { createTracker } from "@offlegacy/event-tracker"; +import { schemas } from "./schemas"; + +export const [Track, useTracker] = createTracker<{}, {}, typeof schemas>({ + schemas: { + schemas, + onSchemaError: (error) => console.error("스키마 검증 실패:", error), + abortOnError: true, + }, +}); +``` + +## 검증된 이벤트 사용하기 + +선언형 컴포넌트와 track 함수로 타입이 보장된 이벤트를 전송합니다. + +```tsx {11, 17-19} +import { Track } from "./event-tracker"; + +function HomePage() { + return ( +
+

홈페이지

+ + {/* ✅ Zod Schema로 검사하여 타입 에러가 발생하지 않습니다. */} + + + {/* 🚨 Zod Schema로 검사하여 타입 에러가 발생합니다. */} + {/* *} + + {/* ✅ Zod Schema로 검사하여 타입 에러가 발생하지 않습니다. */} + + + + + {/* 🚨 Zod Schema로 검사하여 타입 에러가 발생합니다. */} + {/* + + */} +
+ ); +} +``` + +
diff --git a/docs/src/content/ko/index.mdx b/docs/src/content/ko/index.mdx index 97aed6d..ef62205 100644 --- a/docs/src/content/ko/index.mdx +++ b/docs/src/content/ko/index.mdx @@ -1,202 +1,42 @@ -# 소개 - -## `event-tracker`가 필요한 이유 - -이벤트 트래킹은 많은 보일러플레이트 코드와 복잡성을 수반하는 작업입니다. -다음 예시를 살펴보세요. - -```tsx {14,15, 23-30} -// 전통적인 이벤트 트래킹 방식 - -const Page = () => { - const { user, userId } = useUser(); - - return ( -
-

User: {user.name}

- -
- ); -}; - -const Counter = ({ userId }: { userId: string }) => { - // 이벤트 트래킹만을 위해서 'userId'를 prop으로 전달받음 - - const [count, setCount] = useState(0); - const { track } = useTrackEvent(); - - const handleIncrement = () => { - setCount(count + 1); - - track({ - event: "click", - params: { - type: "count", - value: count + 1, - userId, - }, - }); - }; - - return ( -
-

Count: {count}

- -
- ); -}; -``` - -위 코드로 알아본 이벤트 트래킹으로 인한 두 가지 주요 불편사항은 다음과 같습니다: - -1. **Prop Drilling**: `userId`가 `` 컴포넌트에서 `` 컴포넌트로 prop으로 전달됩니다. `` 컴포넌트가 컴포넌트 트리 깊숙이 중첩되어 있다면, prop drilling이 더 심각해져 코드의 가독성과 유지보수성이 저하될 수 있습니다. -2. **이벤트 트래킹 로직과 비즈니스 로직의 강결합**: `handleIncrement` 함수는 카운트 증가 로직과 이벤트 트래킹 로직을 모두 포함하고 있습니다. 이벤트 트래킹 로직을 분리하면 코드를 더 깔끔하고 유지보수하기 쉽게 만들 수 있습니다. - -## `event-tracker`가 제시하는 새로운 패러다임 - -`event-tracker`는 이벤트 트래킹을 위한 새로운 패러다임을 소개합니다. -`event-tracker`가 제시하는 선언적 방식은 전통적으로 이벤트 트래킹과 관련된 복잡성을 단순화하여, -모든 개발자들이 쉽게 접근할 수 있도록 합니다. - -### 선언적 이벤트 트래킹 - -```tsx {5, 10, 24, 26} -const Page = () => { - const { user, userId } = useUser(); - - return ( - -
-

User: {user.name}

- -
-
- ); -}; - -const Counter = () => { - const [count, setCount] = useState(0); - - const handleIncrement = () => { - setCount(count + 1); - }; - - return ( -
-

Count: {count}

- - - -
- ); -}; -``` - -`event-tracker`를 사용하면 선언적 이벤트 트래킹이 가능해져 코드 가독성이 향상되고 복잡성이 감소합니다. 이는 개발자들이 이벤트 트래킹을 더 쉽게 이해하고 사용할 수 있도록 돕습니다. -이제 `handleIncrement` 함수는 카운트 증가에만 책임이 있고, 이벤트 트래킹은 `` 컴포넌트가 처리합니다. -**이러한 선언적 접근 방식은 개발자가 '어떻게 트래킹할지'가 아닌 '무엇을 트래킹할지'에 집중하도록 합니다.** -어떻게 트래킹할지는 React 앱 외부에서 정의되어야 합니다. - -### 애플리케이션 외부에서 이벤트 트래킹 응집도 개선 - -```tsx -const [Track] = createTracker({ - DOMEvents: { - onClick: (params, context) => { - log({ - event: "click", - params: { - ...params, - userId: context.userId, - }, - }); +--- +title: Event Tracker | Comprehensive solution for event tracking in React applications +--- + +import HomePage from "@/components/home-page"; + + { - log({ - event: "impression", - params: { - ...params, - userId: context.userId, - }, - }); - }, -}); -``` - -이제 **'어떻게 추적할지'**에 대한 코드가 비즈니스 로직과 분리되었습니다. -애플리케이션 외부에 위치하므로 비즈니스 로직을 변경하지 않고도 이벤트 트래킹 로직을 수정할 수 있습니다. - -### 데이터 타입 검증 - -`event-tracker`는 [Zod](https://zod.dev/)를 사용하여 스키마 기반으로 데이터 유효성 검증을 제공합니다. - -```tsx -import { z } from "zod"; -import { createTracker } from "@offlegacy/event-tracker"; - -interface Context { - // ... -} - -interface Params { - // ... -} - -// 스키마 정의 -const schemas = { - page_view: z.object({ - title: z.string(), - }), - click_button: z.object({ - target: z.string(), - }), -}; - -// 트래커 설정 -const [Track] = createTracker({ - // 기타 설정... - - schema: { - schemas: { - page_view, - click_button, + { + title: "애널리틱스 도구 독립성", + description: + "Google Analytics, Amplitude 등 어떤 도구든 자유롭게 연동할 수 있어, 기존 인프라를 변경하지 않고도 도입이 가능합니다.", }, - onSchemaError: (error) => { - console.error("Schema validation error:", error); + { + title: "명확한 관심사 분리", + description: + "트래킹 로직을 비즈니스 코드에서 완전히 분리함으로써 코드의 가독성, 테스트 용이성, 유지보수성을 모두 향상시킵니다.", }, - abortOnError: true, - }, -}); - -// 스키마 사용하기 -; -; -``` - -## 주요 기능 - -- 🎯 **타입 안정성을 갖춘 API**: 타입 안전성을 갖춘 선언적 이벤트 트래킹 제공 -- 🛡️ **데이터 타입 검증**: 스키마를 사용한 데이터 타입 안전성과 유효성 보장 -- ⚡️ **최적화된 성능**: 이벤트 배칭을 통한 향상된 성능 -- 🔄 **순서 보장**: 비동기 작업에 대한 실행 순서 보장 -- 🔌 **애널리틱스 도구와의 독립성**: 선택한 모든 애널리틱스 도구와 함께 작동 -- 🧩 **관심사의 분리**: 트래킹 로직과 비즈니스 로직의 분리 유지 -- 📦 **작은 번들 사이즈**: 애플리케이션에 미치는 번들 크기 영향 최소화 - -## 핵심 개념 - -`event-tracker`는 몇 가지 핵심 개념을 기반으로 구축되었습니다: - -1. **Tracker 생성**: `createTracker`를 사용하여 원하는 이벤트 트래킹 지침을 사용하여 트래커 인스턴스를 생성합니다. -2. **Provider**: `Track.Provider`를 사용하여 앱을 래핑하여 컨텍스트를 제공합니다. -3. **이벤트 컴포넌트**: `Track.Click` 또는 `Track.Impression`과 같은 이벤트 컴포넌트를 사용하여 이벤트를 트래킹합니다. -4. **커스텀 Hook**: `useTracker` 훅을 사용하여 명령적으로 트래킹 할 수 있습니다. - -다른 섹션에서 각 기능에 대한 자세한 문서를 확인할 수 있습니다: - -- [createTracker](/docs/create-tracker) - `createTracker`에 대한 자세한 API 문서 -- [components](/docs/components) - 사용 가능한 트래킹 컴포넌트 -- [hook](/docs/hook) - 훅을 사용하여 트래킹 -- [Batching](/docs/batching) - 이벤트 배칭을 통한 성능 최적화 -- [Data Type Validation](/docs/data-type-validation) - 스키마를 사용하여 데이터 타입 안전성과 유효성 보장 + { + title: "최적화된 성능", + description: + "배칭, 디바운스, 스로틀링 등 네트워크 요청을 최소화하는 다양한 전략이 내장되어 있어, 성능 저하 없이 안정적으로 트래킹할 수 있습니다.", + }, + { + title: "실행 순서 보장", + description: + "비동기 상황에서도 이벤트가 의도한 순서대로 처리되도록 설계되어, 복잡한 사용자 흐름에서도 정확한 트래킹이 가능합니다.", + }, + { + title: "데이터 검증", + description: + "Zod 기반의 정적 스키마 검증으로 빌드 타임에서 오류를 방지하고, 수집되는 이벤트 데이터의 신뢰도를 보장합니다.", + }, + ]} +/> diff --git a/docs/src/content/ko/installation.mdx b/docs/src/content/ko/installation.mdx deleted file mode 100644 index 87fba1a..0000000 --- a/docs/src/content/ko/installation.mdx +++ /dev/null @@ -1,19 +0,0 @@ -# 설치 - -npm을 사용하여 설치하기: - -```bash -npm install @offlegacy/event-tracker -``` - -yarn을 사용하여 설치하기: - -```bash -yarn add @offlegacy/event-tracker -``` - -pnpm을 사용하여 설치하기: - -```bash -pnpm add @offlegacy/event-tracker -``` diff --git a/docs/src/content/ko/quick-start.mdx b/docs/src/content/ko/quick-start.mdx deleted file mode 100644 index cbb1b64..0000000 --- a/docs/src/content/ko/quick-start.mdx +++ /dev/null @@ -1,27 +0,0 @@ -# 가이드 - -다음은 `event-tracker`를 사용하는 간단한 예제입니다: - -```tsx -import { createTracker } from "@offlegacy/event-tracker"; - -// 트래커 인스턴스 생성 -const [Track, useTracker] = createTracker({ - DOMEvents: { - onClick: (params, context) => { - log("Click event:", params, context); - }, - }, -}); - -// 앱에서 사용하기 -function App() { - return ( - - - - - - ); -} -``` diff --git a/docs/mdx-components.ts b/docs/src/mdx-components.ts similarity index 80% rename from docs/mdx-components.ts rename to docs/src/mdx-components.ts index 3e463a6..a304c9d 100644 --- a/docs/mdx-components.ts +++ b/docs/src/mdx-components.ts @@ -1,5 +1,5 @@ import { useMDXComponents as getThemeComponents } from "nextra-theme-docs"; -import { MDXComponents } from "nextra/mdx-components"; +import type { MDXComponents } from "nextra/mdx-components"; const themeComponents = getThemeComponents(); diff --git a/docs/src/middleware.ts b/docs/src/middleware.ts index 9b06bd6..9cc1483 100644 --- a/docs/src/middleware.ts +++ b/docs/src/middleware.ts @@ -1,5 +1,15 @@ export { middleware } from "nextra/locales"; export const config = { - matcher: ["/((?!api|_next/static|_next/image|img|favicon.ico|icon.svg|apple-icon.png|manifest|_pagefind).*)"], + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - img (image files) + */ + "/((?!api|_next/static|_next/image|favicon.ico|img|_pagefind).*)", + ], }; diff --git a/package.json b/package.json index 88b7774..4979ed4 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "scripts": { "dev": "tsup --watch", "build": "tsup", + "build:docs": "cd docs && pnpm build", "dev:docs": "cd docs && pnpm dev", "test": "vitest --run", "test:watch": "vitest",