diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml new file mode 100644 index 0000000..dbeb918 --- /dev/null +++ b/.github/workflows/publish-packages.yml @@ -0,0 +1,154 @@ +name: Publish Framework Packages + +on: + workflow_dispatch: + inputs: + package: + description: 'Package to publish (react, nextjs, nextjs-server, vue, svelte, or all)' + required: true + type: choice + options: + - all + - react + - nextjs + - nextjs-server + - vue + - svelte + version: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + dry-run: + description: 'Dry run (do not publish)' + required: false + type: boolean + default: false + +jobs: + publish: + runs-on: ubuntu-latest + strategy: + matrix: + package: ${{ github.event.inputs.package == 'all' && fromJSON('["react", "nextjs", "nextjs-server", "vue", "svelte"]') || fromJSON(format('["{0}"]', github.event.inputs.package)) }} + + permissions: + contents: write + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 10 + run_install: false + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' + + - name: Install root dependencies + run: pnpm install --frozen-lockfile || pnpm install + + - name: Install package dependencies + working-directory: ./${{ matrix.package }} + run: pnpm install || npm install + + - name: Verify package files + working-directory: ./${{ matrix.package }} + run: | + echo "📦 Package contents:" + ls -la + + echo "" + echo "📄 Files to be published:" + npm pack --dry-run 2>&1 | grep -A 100 "package:" + + - name: Bump version + working-directory: ./${{ matrix.package }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + # Get current version + CURRENT_VERSION=$(node -p "require('./package.json').version") + echo "Current version: $CURRENT_VERSION" + + # Bump version + npm version ${{ github.event.inputs.version }} --no-git-tag-version + + # Get new version + NEW_VERSION=$(node -p "require('./package.json').version") + echo "New version: $NEW_VERSION" + echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + + - name: Verify version update + working-directory: ./${{ matrix.package }} + run: | + echo "✅ Version updated to: ${{ env.NEW_VERSION }}" + cat package.json | grep version + + - name: Publish to npm (Dry Run) + if: github.event.inputs.dry-run == 'true' + working-directory: ./${{ matrix.package }} + run: | + echo "🔍 Dry run mode - would publish:" + npm pack --dry-run + ls -la + + - name: Publish to npm + if: github.event.inputs.dry-run != 'true' + working-directory: ./${{ matrix.package }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + # Publish from root directory (includes source files) + npm publish --access public + + - name: Commit version bump + if: github.event.inputs.dry-run != 'true' + run: | + git add ${{ matrix.package }}/package.json + git commit -m "chore(${{ matrix.package }}): release v${{ env.NEW_VERSION }}" || echo "No changes to commit" + git push origin ${{ github.ref_name }} || echo "Nothing to push" + + - name: Create git tag + if: github.event.inputs.dry-run != 'true' + run: | + TAG_NAME="${{ matrix.package }}/v${{ env.NEW_VERSION }}" + git tag -a "$TAG_NAME" -m "Release ${{ matrix.package }} v${{ env.NEW_VERSION }}" + git push origin "$TAG_NAME" + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + + - name: Create GitHub Release + if: github.event.inputs.dry-run != 'true' + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ matrix.package }}/v${{ env.NEW_VERSION }} + release_name: ${{ matrix.package }} v${{ env.NEW_VERSION }} + body: | + Release of @zorihq/${{ matrix.package }} v${{ env.NEW_VERSION }} + + ## Installation + ```bash + npm install @zorihq/${{ matrix.package }}@${{ env.NEW_VERSION }} + ``` + + See the [README](${{ github.server_url }}/${{ github.repository }}/tree/main/${{ matrix.package }}/README.md) for usage instructions. + draft: false + prerelease: false diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..ed859a2 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,228 @@ +# Publishing Framework Libraries + +This document describes how to publish the ZoriHQ framework libraries to npm. + +## Prerequisites + +### 1. NPM Token + +You need to configure an NPM authentication token in GitHub Secrets: + +1. Create an npm account at https://www.npmjs.com/ +2. Generate an automation token: https://www.npmjs.com/settings/YOUR_USERNAME/tokens +3. Add it to GitHub repository secrets as `NPM_TOKEN` + +### 2. Package Scope + +All packages are published under the `@zorihq` scope: +- `@zorihq/react` +- `@zorihq/nextjs` +- `@zorihq/nextjs-server` +- `@zorihq/vue` +- `@zorihq/svelte` + +Ensure you have permission to publish to this scope on npm. + +## Publishing Workflow + +### Automatic Publishing via GitHub Actions + +The workflow `publish-packages.yml` handles building, versioning, and publishing packages to npm. + +#### Publish a Single Package + +1. Go to **Actions** → **Publish Framework Packages** +2. Click **Run workflow** +3. Select options: + - **Package**: Choose which package to publish (react, nextjs, nextjs-server, vue, svelte) + - **Version bump type**: Choose `patch`, `minor`, or `major` + - **Dry run**: Check this to test without actually publishing + +#### Publish All Packages + +1. Go to **Actions** → **Publish Framework Packages** +2. Click **Run workflow** +3. Select options: + - **Package**: Choose `all` + - **Version bump type**: Choose `patch`, `minor`, or `major` + - **Dry run**: Check this to test without actually publishing + +### What the Workflow Does + +1. **Checks out code** from the repository +2. **Installs dependencies** for the selected package(s) +3. **Verifies package files** to ensure everything is ready +4. **Bumps version** according to your selection (patch, minor, major) +5. **Publishes to npm** with public access +6. **Commits version bump** back to the repository +7. **Creates git tag** in format `{package}/v{version}` (e.g., `react/v1.0.1`) +8. **Creates GitHub Release** with installation instructions + +## Manual Publishing (Local) + +If you need to publish manually from your local machine: + +### 1. Login to npm + +```bash +npm login +``` + +### 2. Navigate to Package Directory + +```bash +cd react # or nextjs, nextjs-server, vue, svelte +``` + +### 3. Bump Version + +```bash +npm version patch # or minor, major +``` + +### 4. Publish + +```bash +npm publish --access public +``` + +### 5. Tag and Push + +```bash +git add package.json +git commit -m "chore(react): release v1.0.1" +git tag react/v1.0.1 +git push origin main --tags +``` + +## Version Bump Guidelines + +Follow semantic versioning: + +- **patch** (1.0.0 → 1.0.1): Bug fixes, small improvements +- **minor** (1.0.0 → 1.1.0): New features, backward compatible +- **major** (1.0.0 → 2.0.0): Breaking changes + +## Package Structure + +Each package is published with source files (TypeScript/TSX) to allow users' build systems to handle compilation: + +``` +@zorihq/react/ +├── index.tsx # Main source file +├── README.md # Usage documentation +├── package.json # Package manifest +└── tsconfig.json # TypeScript config +``` + +## Verifying Published Packages + +After publishing, verify on npm: + +```bash +npm view @zorihq/react +npm view @zorihq/nextjs +npm view @zorihq/nextjs-server +npm view @zorihq/vue +npm view @zorihq/svelte +``` + +Or visit: +- https://www.npmjs.com/package/@zorihq/react +- https://www.npmjs.com/package/@zorihq/nextjs +- https://www.npmjs.com/package/@zorihq/nextjs-server +- https://www.npmjs.com/package/@zorihq/vue +- https://www.npmjs.com/package/@zorihq/svelte + +## Testing Before Publishing + +### Dry Run with GitHub Actions + +Always test with dry-run enabled first: + +1. Run the workflow with **dry-run: true** +2. Check the workflow logs to see what would be published +3. If everything looks good, run again with **dry-run: false** + +### Local Testing + +Test package locally before publishing: + +```bash +cd react +npm pack +# This creates a .tgz file you can inspect or install locally +``` + +Install the packed version in a test project: + +```bash +npm install /path/to/zorihq-react-1.0.0.tgz +``` + +## Troubleshooting + +### "You do not have permission to publish" + +- Ensure you're logged into the correct npm account +- Ensure you have access to the `@zorihq` scope +- Check that `NPM_TOKEN` secret is correctly configured + +### "Version already exists" + +- You're trying to publish a version that's already on npm +- Bump the version first with `npm version patch/minor/major` + +### "Files not included in package" + +- Check the `files` field in `package.json` +- Run `npm pack --dry-run` to see what would be included +- Verify source files exist in the package directory + +## CI/CD Integration + +The workflow integrates with GitHub's CI/CD: + +- **On workflow_dispatch**: Manual trigger from GitHub Actions UI +- **Permissions**: Requires `contents: write` and `packages: write` +- **Matrix strategy**: Can publish multiple packages in parallel when `all` is selected + +## Post-Publishing + +After publishing: + +1. **Update documentation** if needed +2. **Announce release** in relevant channels +3. **Monitor npm** for download stats +4. **Watch for issues** reported by users + +## Best Practices + +1. ✅ **Always use dry-run first** for new packages +2. ✅ **Test locally** before publishing +3. ✅ **Follow semantic versioning** strictly +4. ✅ **Update README** with breaking changes +5. ✅ **Tag releases** with descriptive names +6. ✅ **Keep changelogs** updated +7. ✅ **Coordinate releases** when updating multiple packages + +## Package URLs + +After publishing, packages are available at: + +- **npm**: `https://www.npmjs.com/package/@zorihq/{package-name}` +- **GitHub**: `https://github.com/ZoriHQ/script/tree/main/{package-name}` +- **Unpkg CDN**: `https://unpkg.com/@zorihq/{package-name}` (for bundled builds) + +## Support + +For issues with publishing: + +1. Check GitHub Actions logs for detailed error messages +2. Verify npm credentials and permissions +3. Review package.json configuration +4. Test with `npm pack --dry-run` locally + +## License + +All packages are published under the MIT License. diff --git a/nextjs/README.md b/nextjs/README.md new file mode 100644 index 0000000..de45071 --- /dev/null +++ b/nextjs/README.md @@ -0,0 +1,220 @@ +# @zorihq/nextjs + +Next.js client-side hooks and components for ZoriHQ Analytics. + +## Installation + +```bash +npm install @zorihq/nextjs +# or +pnpm add @zorihq/nextjs +# or +yarn add @zorihq/nextjs +``` + +## Usage with App Router (Next.js 13+) + +### 1. Create a Client Component Wrapper + +Create `app/providers.tsx`: + +```tsx +'use client'; + +import { ZoriProvider } from '@zorihq/nextjs'; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` + +### 2. Wrap Your Root Layout + +Update `app/layout.tsx`: + +```tsx +import { Providers } from './providers'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} +``` + +### 3. Use in Client Components + +```tsx +'use client'; + +import { useZori } from '@zorihq/nextjs'; + +export default function MyPage() { + const { track, identify } = useZori(); + + const handlePurchase = async () => { + await track('purchase_completed', { + product_id: 'prod_123', + amount: 99.99, + }); + }; + + return ( + + ); +} +``` + +## Usage with Pages Router (Next.js 12) + +### 1. Wrap Your App + +Update `pages/_app.tsx`: + +```tsx +import { ZoriProvider } from '@zorihq/nextjs'; +import type { AppProps } from 'next/app'; + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ); +} +``` + +## Features + +### Auto Route Tracking + +The `ZoriProvider` automatically tracks route changes in both App Router and Pages Router when `autoTrackPageViews={true}` is set. + +### Track Events + +```tsx +'use client'; + +import { useZori } from '@zorihq/nextjs'; + +function MyComponent() { + const { track } = useZori(); + + const handleClick = () => { + track('button_clicked', { button_name: 'signup' }); + }; + + return ; +} +``` + +### Identify Users + +```tsx +'use client'; + +import { useIdentify } from '@zorihq/nextjs'; + +function UserProfile({ user }) { + useIdentify(user ? { + app_id: user.id, + email: user.email, + fullname: user.name, + } : null); + + return
{user?.name}
; +} +``` + +### TrackClick Component + +```tsx +'use client'; + +import { TrackClick } from '@zorihq/nextjs'; + +function MyButton() { + return ( + + Get Started + + ); +} +``` + +## Environment Variables + +Create `.env.local`: + +```env +NEXT_PUBLIC_ZORI_KEY=your-publishable-key +``` + +## API Reference + +### ZoriProvider Props + +- `config` (required): Configuration object + - `publishableKey` (required): Your ZoriHQ publishable key + - `baseUrl` (optional): Custom ingestion endpoint + - `comebackThreshold` (optional): Minimum time away to trigger comeback event (ms) + - `trackQuickSwitches` (optional): Track all visibility changes +- `children` (required): Your app components +- `autoTrackPageViews` (optional): Auto-track page views on route change, default `true` + +### useZori Hook + +Returns an object with: + +- `isInitialized`: Boolean indicating if ZoriHQ is ready +- `track(eventName, properties)`: Track custom events +- `identify(userInfo)`: Identify users +- `getVisitorId()`: Get the visitor ID +- `getSessionId()`: Get the current session ID +- `setConsent(preferences)`: Set GDPR consent preferences +- `optOut()`: Opt out of tracking completely +- `hasConsent()`: Check if user has given consent + +## TypeScript Support + +This package includes TypeScript definitions out of the box. + +## Server-Side Tracking + +For server-side tracking (Server Components, API Routes, Middleware), use `@zorihq/nextjs-server` instead. + +## License + +MIT diff --git a/nextjs/index.tsx b/nextjs/index.tsx new file mode 100644 index 0000000..3528494 --- /dev/null +++ b/nextjs/index.tsx @@ -0,0 +1,259 @@ +'use client'; + +import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'; +import { usePathname, useSearchParams } from 'next/navigation'; +import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types'; + +// Re-export shared types for convenience +export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types'; + +// Next.js-specific context type extending core API +export interface ZoriContextType extends ZoriCoreAPI { + isInitialized: boolean; +} + +// Context +const ZoriContext = createContext(null); + +// Provider Props +export interface ZoriProviderProps { + config: ZoriConfig; + children: React.ReactNode; + autoTrackPageViews?: boolean; +} + +// Provider Component +export const ZoriProvider: React.FC = ({ + config, + children, + autoTrackPageViews = true, +}) => { + const [isInitialized, setIsInitialized] = useState(false); + const scriptLoadedRef = useRef(false); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + useEffect(() => { + if (scriptLoadedRef.current) return; + + // Initialize queue + (window as any).ZoriHQ = (window as any).ZoriHQ || []; + + // Load script + const script = document.createElement('script'); + script.src = 'https://cdn.zorihq.com/script.min.js'; + script.async = true; + script.setAttribute('data-key', config.publishableKey); + + if (config.baseUrl) { + script.setAttribute('data-base-url', config.baseUrl); + } + + if (config.comebackThreshold !== undefined) { + script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString()); + } + + if (config.trackQuickSwitches !== undefined) { + script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString()); + } + + script.onload = () => { + setIsInitialized(true); + }; + + document.head.appendChild(script); + scriptLoadedRef.current = true; + + return () => { + if (script.parentNode) { + script.parentNode.removeChild(script); + } + }; + }, [config]); + + // Auto-track page views on route change (App Router) + useEffect(() => { + if (isInitialized && autoTrackPageViews) { + const zori = (window as any).ZoriHQ; + if (zori) { + const properties = { + page_title: document.title, + page_path: pathname, + page_search: searchParams?.toString() || '', + page_hash: window.location.hash, + }; + + if (typeof zori.track === 'function') { + zori.track('page_view', properties); + } else { + zori.push(['track', 'page_view', properties]); + } + } + } + }, [pathname, searchParams, isInitialized, autoTrackPageViews]); + + const track = useCallback(async (eventName: string, properties?: Record) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.track === 'function') { + return await zori.track(eventName, properties); + } else { + zori.push(['track', eventName, properties]); + return true; + } + }, []); + + const identify = useCallback(async (userInfo: UserInfo) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.identify === 'function') { + return await zori.identify(userInfo); + } else { + zori.push(['identify', userInfo]); + return true; + } + }, []); + + const getVisitorId = useCallback(async () => { + const zori = (window as any).ZoriHQ; + if (!zori) return ''; + + if (typeof zori.getVisitorId === 'function') { + return await zori.getVisitorId(); + } + + return new Promise((resolve) => { + zori.push(['getVisitorId', (id: string) => resolve(id)]); + }); + }, []); + + const getSessionId = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.getSessionId !== 'function') return null; + return zori.getSessionId(); + }, []); + + const setConsent = useCallback((preferences: ConsentPreferences) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.setConsent === 'function') { + return zori.setConsent(preferences); + } else { + zori.push(['setConsent', preferences]); + return true; + } + }, []); + + const optOut = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.optOut === 'function') { + return zori.optOut(); + } else { + zori.push(['optOut']); + return true; + } + }, []); + + const hasConsent = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.hasConsent !== 'function') return true; + return zori.hasConsent(); + }, []); + + const contextValue: ZoriContextType = { + isInitialized, + track, + identify, + getVisitorId, + getSessionId, + setConsent, + optOut, + hasConsent, + }; + + return {children}; +}; + +// Hook to use Zori +export const useZori = (): ZoriContextType => { + const context = useContext(ZoriContext); + if (!context) { + throw new Error('useZori must be used within a ZoriProvider'); + } + return context; +}; + +// Hook to track events with dependencies +export const useTrackEvent = ( + eventName: string, + properties?: Record, + dependencies: any[] = [] +) => { + const { track, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized) { + track(eventName, properties); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isInitialized, ...dependencies]); +}; + +// Hook to identify user +export const useIdentify = (userInfo: UserInfo | null) => { + const { identify, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized && userInfo) { + identify(userInfo); + } + }, [isInitialized, userInfo, identify]); +}; + +// Component to track clicks +export interface TrackClickProps { + eventName?: string; + properties?: Record; + children: React.ReactNode; + as?: React.ElementType; + [key: string]: any; +} + +export const TrackClick: React.FC = ({ + eventName = 'click', + properties = {}, + children, + as: Component = 'button', + ...props +}) => { + const { track } = useZori(); + + const handleClick = useCallback( + (event: React.MouseEvent) => { + track(eventName, properties); + if (props.onClick) { + props.onClick(event); + } + }, + [track, eventName, properties, props] + ); + + return ( + + {children} + + ); +}; + +export default { + ZoriProvider, + useZori, + useTrackEvent, + useIdentify, + TrackClick, +}; diff --git a/nextjs/package.json b/nextjs/package.json new file mode 100644 index 0000000..349c17a --- /dev/null +++ b/nextjs/package.json @@ -0,0 +1,51 @@ +{ + "name": "@zorihq/nextjs", + "version": "1.0.0", + "description": "ZoriHQ Analytics for Next.js (Client-side)", + "main": "index.tsx", + "types": "index.tsx", + "exports": { + ".": { + "import": "./index.tsx", + "require": "./index.tsx", + "types": "./index.tsx" + } + }, + "files": [ + "index.tsx", + "README.md", + "package.json" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "nextjs", + "next", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "next": ">=13.0.0", + "react": ">=18.0.0" + }, + "dependencies": { + "@zorihq/types": "^1.0.0" + }, + "devDependencies": { + "@types/react": "^18.0.0", + "next": "^14.0.0", + "react": "^18.0.0", + "typescript": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "nextjs" + } +} diff --git a/nextjs/pnpm-lock.yaml b/nextjs/pnpm-lock.yaml new file mode 100644 index 0000000..3a7a328 --- /dev/null +++ b/nextjs/pnpm-lock.yaml @@ -0,0 +1,321 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@zorihq/types': + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + '@types/react': + specifier: ^18.0.0 + version: 18.3.27 + next: + specifier: ^14.0.0 + version: 14.2.33(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.0.0 + version: 18.3.1 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + +packages: + + '@next/env@14.2.33': + resolution: {integrity: sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==} + + '@next/swc-darwin-arm64@14.2.33': + resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@14.2.33': + resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@14.2.33': + resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@14.2.33': + resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@14.2.33': + resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@14.2.33': + resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@14.2.33': + resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.2.33': + resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@14.2.33': + resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.5': + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@zorihq/types@1.0.0': + resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + caniuse-lite@1.0.30001756: + resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@14.2.33: + resolution: {integrity: sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@next/env@14.2.33': {} + + '@next/swc-darwin-arm64@14.2.33': + optional: true + + '@next/swc-darwin-x64@14.2.33': + optional: true + + '@next/swc-linux-arm64-gnu@14.2.33': + optional: true + + '@next/swc-linux-arm64-musl@14.2.33': + optional: true + + '@next/swc-linux-x64-gnu@14.2.33': + optional: true + + '@next/swc-linux-x64-musl@14.2.33': + optional: true + + '@next/swc-win32-arm64-msvc@14.2.33': + optional: true + + '@next/swc-win32-ia32-msvc@14.2.33': + optional: true + + '@next/swc-win32-x64-msvc@14.2.33': + optional: true + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.5': + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.8.1 + + '@types/prop-types@15.7.15': {} + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@zorihq/types@1.0.0': {} + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + caniuse-lite@1.0.30001756: {} + + client-only@0.0.1: {} + + csstype@3.2.3: {} + + graceful-fs@4.2.11: {} + + js-tokens@4.0.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + nanoid@3.3.11: {} + + next@14.2.33(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 14.2.33 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001756 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.33 + '@next/swc-darwin-x64': 14.2.33 + '@next/swc-linux-arm64-gnu': 14.2.33 + '@next/swc-linux-arm64-musl': 14.2.33 + '@next/swc-linux-x64-gnu': 14.2.33 + '@next/swc-linux-x64-musl': 14.2.33 + '@next/swc-win32-arm64-msvc': 14.2.33 + '@next/swc-win32-ia32-msvc': 14.2.33 + '@next/swc-win32-x64-msvc': 14.2.33 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + picocolors@1.1.1: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + source-map-js@1.2.1: {} + + streamsearch@1.1.0: {} + + styled-jsx@5.1.1(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + + tslib@2.8.1: {} + + typescript@5.9.3: {} diff --git a/nextjs/tsconfig.json b/nextjs/tsconfig.json new file mode 100644 index 0000000..eee058f --- /dev/null +++ b/nextjs/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/react/README.md b/react/README.md new file mode 100644 index 0000000..3148aff --- /dev/null +++ b/react/README.md @@ -0,0 +1,203 @@ +# @zorihq/react + +React hooks and components for ZoriHQ Analytics. + +## Installation + +```bash +npm install @zorihq/react +# or +pnpm add @zorihq/react +# or +yarn add @zorihq/react +``` + +## Usage + +### 1. Wrap your app with ZoriProvider + +```tsx +import { ZoriProvider } from '@zorihq/react'; + +function App() { + return ( + + + + ); +} +``` + +### 2. Use the hooks + +#### useZori - Main Hook + +```tsx +import { useZori } from '@zorihq/react'; + +function MyComponent() { + const { track, identify, getVisitorId, setConsent, optOut } = useZori(); + + const handlePurchase = async () => { + await track('purchase_completed', { + product_id: 'prod_123', + amount: 99.99, + }); + }; + + const handleLogin = async () => { + await identify({ + app_id: 'user_123', + email: 'user@example.com', + fullname: 'John Doe', + }); + }; + + return ( +
+ + +
+ ); +} +``` + +#### usePageView - Auto Track Page Views + +```tsx +import { usePageView } from '@zorihq/react'; + +function ProductPage({ productId }) { + usePageView({ + product_id: productId, + page_type: 'product', + }); + + return
Product {productId}
; +} +``` + +#### useIdentify - Auto Identify Users + +```tsx +import { useIdentify } from '@zorihq/react'; + +function UserProfile({ user }) { + useIdentify(user ? { + app_id: user.id, + email: user.email, + fullname: user.name, + plan: user.subscription + } : null); + + return
{user?.name}
; +} +``` + +#### useTrackEvent - Track Events with Dependencies + +```tsx +import { useTrackEvent } from '@zorihq/react'; + +function SearchResults({ query, results }) { + useTrackEvent( + 'search_completed', + { + query, + result_count: results.length, + }, + [query, results.length] // Re-track when these change + ); + + return
{results.length} results for "{query}"
; +} +``` + +### 3. TrackClick Component + +Automatically track clicks on any element: + +```tsx +import { TrackClick } from '@zorihq/react'; + +function MyButton() { + return ( + + Sign Up + + ); +} + +// Works with any element +function MyLink() { + return ( + + Learn More + + ); +} +``` + +## API Reference + +### ZoriProvider Props + +- `config` (required): Configuration object + - `publishableKey` (required): Your ZoriHQ publishable key + - `baseUrl` (optional): Custom ingestion endpoint + - `comebackThreshold` (optional): Minimum time away to trigger comeback event (ms) + - `trackQuickSwitches` (optional): Track all visibility changes +- `children` (required): Your app components +- `autoTrackPageViews` (optional): Auto-track page views on mount, default `true` + +### useZori Hook + +Returns an object with: + +- `isInitialized`: Boolean indicating if ZoriHQ is ready +- `track(eventName, properties)`: Track custom events +- `identify(userInfo)`: Identify users +- `getVisitorId()`: Get the visitor ID +- `getSessionId()`: Get the current session ID +- `setConsent(preferences)`: Set GDPR consent preferences +- `optOut()`: Opt out of tracking completely +- `hasConsent()`: Check if user has given consent + +## TypeScript Support + +This package includes TypeScript definitions out of the box. + +```tsx +import { ZoriConfig, UserInfo, ConsentPreferences } from '@zorihq/react'; + +const config: ZoriConfig = { + publishableKey: 'your-key', +}; + +const user: UserInfo = { + app_id: 'user_123', + email: 'user@example.com', +}; +``` + +## License + +MIT diff --git a/react/index.tsx b/react/index.tsx new file mode 100644 index 0000000..960cc32 --- /dev/null +++ b/react/index.tsx @@ -0,0 +1,251 @@ +import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'; +import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types'; + +// Re-export shared types for convenience +export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types'; + +// React-specific context type extending core API +export interface ZoriContextType extends ZoriCoreAPI { + isInitialized: boolean; +} + +// Context +const ZoriContext = createContext(null); + +// Provider Props +export interface ZoriProviderProps { + config: ZoriConfig; + children: React.ReactNode; + autoTrackPageViews?: boolean; +} + +// Provider Component +export const ZoriProvider: React.FC = ({ + config, + children, + autoTrackPageViews = true, +}) => { + const [isInitialized, setIsInitialized] = useState(false); + const scriptLoadedRef = useRef(false); + + useEffect(() => { + if (scriptLoadedRef.current) return; + + // Initialize queue + (window as any).ZoriHQ = (window as any).ZoriHQ || []; + + // Load script + const script = document.createElement('script'); + script.src = 'https://cdn.zorihq.com/script.min.js'; + script.async = true; + script.setAttribute('data-key', config.publishableKey); + + if (config.baseUrl) { + script.setAttribute('data-base-url', config.baseUrl); + } + + if (config.comebackThreshold !== undefined) { + script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString()); + } + + if (config.trackQuickSwitches !== undefined) { + script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString()); + } + + script.onload = () => { + setIsInitialized(true); + }; + + document.head.appendChild(script); + scriptLoadedRef.current = true; + + return () => { + if (script.parentNode) { + script.parentNode.removeChild(script); + } + }; + }, [config]); + + const track = useCallback(async (eventName: string, properties?: Record) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.track === 'function') { + return await zori.track(eventName, properties); + } else { + zori.push(['track', eventName, properties]); + return true; + } + }, []); + + const identify = useCallback(async (userInfo: UserInfo) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.identify === 'function') { + return await zori.identify(userInfo); + } else { + zori.push(['identify', userInfo]); + return true; + } + }, []); + + const getVisitorId = useCallback(async () => { + const zori = (window as any).ZoriHQ; + if (!zori) return ''; + + if (typeof zori.getVisitorId === 'function') { + return await zori.getVisitorId(); + } + + return new Promise((resolve) => { + zori.push(['getVisitorId', (id: string) => resolve(id)]); + }); + }, []); + + const getSessionId = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.getSessionId !== 'function') return null; + return zori.getSessionId(); + }, []); + + const setConsent = useCallback((preferences: ConsentPreferences) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.setConsent === 'function') { + return zori.setConsent(preferences); + } else { + zori.push(['setConsent', preferences]); + return true; + } + }, []); + + const optOut = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.optOut === 'function') { + return zori.optOut(); + } else { + zori.push(['optOut']); + return true; + } + }, []); + + const hasConsent = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.hasConsent !== 'function') return true; + return zori.hasConsent(); + }, []); + + const contextValue: ZoriContextType = { + isInitialized, + track, + identify, + getVisitorId, + getSessionId, + setConsent, + optOut, + hasConsent, + }; + + return {children}; +}; + +// Hook to use Zori +export const useZori = (): ZoriContextType => { + const context = useContext(ZoriContext); + if (!context) { + throw new Error('useZori must be used within a ZoriProvider'); + } + return context; +}; + +// Hook to track page views +export const usePageView = (properties?: Record) => { + const { track, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized) { + track('page_view', { + page_title: document.title, + page_path: window.location.pathname, + page_search: window.location.search, + page_hash: window.location.hash, + ...properties, + }); + } + }, [isInitialized, track, properties]); +}; + +// Hook to track events with dependencies +export const useTrackEvent = ( + eventName: string, + properties?: Record, + dependencies: any[] = [] +) => { + const { track, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized) { + track(eventName, properties); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isInitialized, ...dependencies]); +}; + +// Hook to identify user +export const useIdentify = (userInfo: UserInfo | null) => { + const { identify, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized && userInfo) { + identify(userInfo); + } + }, [isInitialized, userInfo, identify]); +}; + +// Component to track clicks +export interface TrackClickProps { + eventName?: string; + properties?: Record; + children: React.ReactNode; + as?: React.ElementType; + [key: string]: any; +} + +export const TrackClick: React.FC = ({ + eventName = 'click', + properties = {}, + children, + as: Component = 'button', + ...props +}) => { + const { track } = useZori(); + + const handleClick = useCallback( + (event: React.MouseEvent) => { + track(eventName, properties); + if (props.onClick) { + props.onClick(event); + } + }, + [track, eventName, properties, props] + ); + + return ( + + {children} + + ); +}; + +export default { + ZoriProvider, + useZori, + usePageView, + useTrackEvent, + useIdentify, + TrackClick, +}; diff --git a/react/package.json b/react/package.json new file mode 100644 index 0000000..eb62969 --- /dev/null +++ b/react/package.json @@ -0,0 +1,48 @@ +{ + "name": "@zorihq/react", + "version": "1.0.0", + "description": "ZoriHQ Analytics for React", + "main": "index.tsx", + "types": "index.tsx", + "exports": { + ".": { + "import": "./index.tsx", + "require": "./index.tsx", + "types": "./index.tsx" + } + }, + "files": [ + "index.tsx", + "README.md", + "package.json" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "react", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + }, + "dependencies": { + "@zorihq/types": "^1.0.0" + }, + "devDependencies": { + "@types/react": "^18.0.0", + "react": "^18.0.0", + "typescript": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "react" + } +} diff --git a/react/pnpm-lock.yaml b/react/pnpm-lock.yaml new file mode 100644 index 0000000..15f49cc --- /dev/null +++ b/react/pnpm-lock.yaml @@ -0,0 +1,78 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@zorihq/types': + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + '@types/react': + specifier: ^18.0.0 + version: 18.3.27 + react: + specifier: ^18.0.0 + version: 18.3.1 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + +packages: + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@zorihq/types@1.0.0': + resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@types/prop-types@15.7.15': {} + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@zorihq/types@1.0.0': {} + + csstype@3.2.3: {} + + js-tokens@4.0.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + typescript@5.9.3: {} diff --git a/react/tsconfig.json b/react/tsconfig.json new file mode 100644 index 0000000..eee058f --- /dev/null +++ b/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 0000000..42f6b0c --- /dev/null +++ b/shared/package.json @@ -0,0 +1,34 @@ +{ + "name": "@zorihq/types", + "version": "1.0.0", + "description": "Shared TypeScript types for ZoriHQ Analytics SDKs", + "main": "types.ts", + "types": "types.ts", + "exports": { + ".": { + "import": "./types.ts", + "require": "./types.ts", + "types": "./types.ts" + } + }, + "files": [ + "types.ts", + "package.json" + ], + "scripts": { + "prepublishOnly": "echo 'Publishing source files'" + }, + "keywords": [ + "analytics", + "types", + "typescript", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "shared" + } +} diff --git a/shared/types.ts b/shared/types.ts new file mode 100644 index 0000000..f640cd0 --- /dev/null +++ b/shared/types.ts @@ -0,0 +1,132 @@ +/** + * Shared TypeScript types for ZoriHQ SDKs + * These types are reused across React, Next.js, Vue, Svelte, and server-side implementations + */ + +// ============================================================================= +// Core Configuration Types +// ============================================================================= + +/** + * Configuration options for ZoriHQ client-side SDKs + */ +export interface ZoriConfig { + /** Your ZoriHQ publishable key */ + publishableKey: string; + /** Custom API base URL (optional) */ + baseUrl?: string; + /** Threshold in ms for detecting user comebacks (optional) */ + comebackThreshold?: number; + /** Whether to track quick tab switches (optional) */ + trackQuickSwitches?: boolean; +} + +/** + * Configuration options for ZoriHQ server-side SDK + * A subset of ZoriConfig without client-specific options + */ +export interface ZoriServerConfig { + /** Your ZoriHQ publishable key */ + publishableKey: string; + /** Custom API base URL (optional) */ + baseUrl?: string; +} + +// ============================================================================= +// User & Consent Types +// ============================================================================= + +/** + * User consent preferences for tracking + */ +export interface ConsentPreferences { + /** Whether analytics tracking is allowed */ + analytics?: boolean; + /** Whether marketing tracking is allowed */ + marketing?: boolean; +} + +/** + * User identification information + */ +export interface UserInfo { + /** Application-specific user ID */ + app_id?: string; + /** User's email address */ + email?: string; + /** User's full name (alternative 1) */ + fullname?: string; + /** User's full name (alternative 2) */ + full_name?: string; + /** Additional custom properties */ + [key: string]: any; +} + +// ============================================================================= +// Core API Interface +// ============================================================================= + +/** + * Core API methods shared across all ZoriHQ client SDKs + * Framework-specific implementations extend or implement this interface + */ +export interface ZoriCoreAPI { + /** Track a custom event */ + track: (eventName: string, properties?: Record) => Promise; + /** Identify a user */ + identify: (userInfo: UserInfo) => Promise; + /** Get the current visitor ID */ + getVisitorId: () => Promise; + /** Get the current session ID */ + getSessionId: () => string | null; + /** Set user consent preferences */ + setConsent: (preferences: ConsentPreferences) => boolean; + /** Opt out of all tracking */ + optOut: () => boolean; + /** Check if user has given consent */ + hasConsent: () => boolean; +} + +// ============================================================================= +// Server-Side Types +// ============================================================================= + +/** + * Options for tracking events on the server side + */ +export interface TrackEventOptions { + /** Name of the event to track */ + eventName: string; + /** Custom properties to attach to the event */ + properties?: Record; + /** Override visitor ID (optional, auto-generated if not provided) */ + visitorId?: string; + /** Override session ID (optional, auto-generated if not provided) */ + sessionId?: string; + /** User agent string */ + userAgent?: string; + /** Full page URL */ + pageUrl?: string; + /** Host/domain name */ + host?: string; + /** Referrer URL */ + referrer?: string; +} + +/** + * Options for identifying users on the server side + */ +export interface IdentifyOptions { + /** User information to associate with the visitor */ + userInfo: UserInfo; + /** Override visitor ID (optional, auto-generated if not provided) */ + visitorId?: string; + /** Override session ID (optional, auto-generated if not provided) */ + sessionId?: string; + /** User agent string */ + userAgent?: string; + /** Full page URL */ + pageUrl?: string; + /** Host/domain name */ + host?: string; +} diff --git a/svelte/README.md b/svelte/README.md new file mode 100644 index 0000000..c1f0510 --- /dev/null +++ b/svelte/README.md @@ -0,0 +1,333 @@ +# @zorihq/svelte + +Svelte stores, actions, and components for ZoriHQ Analytics. + +## Installation + +```bash +npm install @zorihq/svelte +# or +pnpm add @zorihq/svelte +# or +yarn add @zorihq/svelte +``` + +## Usage + +### 1. Initialize Zori + +#### Option A: Global Store (Recommended) + +Initialize once in your root component or layout: + +```svelte + + + + +``` + +#### Option B: Component Context + +Use the `ZoriProvider` component: + +```svelte + + + + + + +``` + +### 2. Track Events + +```svelte + + + + +``` + +### 3. Use Actions + +#### trackClick Action + +Automatically track clicks on any element: + +```svelte + + + + + + View Pricing + +``` + +### 4. Use Helpers + +#### usePageView + +Auto-track page views on component mount: + +```svelte + + +
Product {productId}
+``` + +#### useTrackEvent + +Track events on component mount: + +```svelte + + +
User Profile
+``` + +#### useIdentify + +Identify users on component mount: + +```svelte + + +
{user?.name}
+``` + +### 5. Reactive Tracking + +```svelte + + + +
{results.length} results
+``` + +### 6. Check Initialization Status + +```svelte + + +{#if $zori.isInitialized} +

Analytics Ready!

+{:else} +

Loading analytics...

+{/if} +``` + +## SvelteKit Integration + +### +layout.svelte + +```svelte + + + +``` + +### Environment Variables + +Create `.env`: + +```env +VITE_ZORI_KEY=your-publishable-key +``` + +## API Reference + +### createZoriStore(config) + +Create a new Zori store instance. + +```typescript +import { createZoriStore } from '@zorihq/svelte'; + +const zori = createZoriStore({ + publishableKey: 'your-key', + baseUrl: 'https://ingestion.zorihq.com/ingest', // optional + comebackThreshold: 30000, // optional + trackQuickSwitches: false, // optional +}); +``` + +### initZori(config) + +Initialize global Zori store (recommended for app-wide usage). + +```typescript +import { initZori } from '@zorihq/svelte'; + +const zori = initZori({ publishableKey: 'your-key' }); +``` + +### getZori() + +Get the global Zori store instance. + +```typescript +import { getZori } from '@zorihq/svelte'; + +const zori = getZori(); +``` + +### ZoriStore + +The store exposes: + +- `isInitialized`: Readable store indicating if ZoriHQ is ready +- `track(eventName, properties)`: Track custom events +- `identify(userInfo)`: Identify users +- `getVisitorId()`: Get the visitor ID +- `getSessionId()`: Get the current session ID +- `setConsent(preferences)`: Set GDPR consent preferences +- `optOut()`: Opt out of tracking completely +- `hasConsent()`: Check if user has given consent + +### Actions + +#### trackClick + +```svelte + +``` + +### Helpers + +- `usePageView(properties?)`: Track page view on mount +- `useTrackEvent(eventName, properties?)`: Track event on mount +- `useIdentify(userInfo)`: Identify user on mount + +## TypeScript Support + +This package includes full TypeScript support: + +```typescript +import type { + ZoriConfig, + ZoriStore, + ConsentPreferences, + UserInfo, +} from '@zorihq/svelte'; +``` + +## Stores Pattern + +This library follows Svelte's stores pattern: + +- Use `$` to auto-subscribe in components +- Stores are reactive and update automatically +- Automatic cleanup on component destroy + +## License + +MIT diff --git a/svelte/ZoriProvider.svelte b/svelte/ZoriProvider.svelte new file mode 100644 index 0000000..f5daad8 --- /dev/null +++ b/svelte/ZoriProvider.svelte @@ -0,0 +1,14 @@ + + + diff --git a/svelte/index.ts b/svelte/index.ts new file mode 100644 index 0000000..fa2900b --- /dev/null +++ b/svelte/index.ts @@ -0,0 +1,236 @@ +import { writable, readonly, derived, get, type Readable, type Writable } from 'svelte/store'; +import { onMount, onDestroy } from 'svelte'; +import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types'; + +// Re-export shared types for convenience +export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types'; + +// Svelte-specific store type extending core API with reactive state +export interface ZoriStore extends Omit { + isInitialized: Readable; + getSessionId: () => string | null; +} + +// Create the Zori store +export function createZoriStore(config: ZoriConfig): ZoriStore { + const isInitialized = writable(false); + let scriptLoaded = false; + + const loadScript = () => { + if (scriptLoaded || typeof window === 'undefined') return; + + // Initialize queue + (window as any).ZoriHQ = (window as any).ZoriHQ || []; + + // Load script + const script = document.createElement('script'); + script.src = 'https://cdn.zorihq.com/script.min.js'; + script.async = true; + script.setAttribute('data-key', config.publishableKey); + + if (config.baseUrl) { + script.setAttribute('data-base-url', config.baseUrl); + } + + if (config.comebackThreshold !== undefined) { + script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString()); + } + + if (config.trackQuickSwitches !== undefined) { + script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString()); + } + + script.onload = () => { + isInitialized.set(true); + }; + + document.head.appendChild(script); + scriptLoaded = true; + }; + + const track = async (eventName: string, properties?: Record): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.track === 'function') { + return await zori.track(eventName, properties); + } else { + zori.push(['track', eventName, properties]); + return true; + } + }; + + const identify = async (userInfo: UserInfo): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.identify === 'function') { + return await zori.identify(userInfo); + } else { + zori.push(['identify', userInfo]); + return true; + } + }; + + const getVisitorId = async (): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return ''; + + if (typeof zori.getVisitorId === 'function') { + return await zori.getVisitorId(); + } + + return new Promise((resolve) => { + zori.push(['getVisitorId', (id: string) => resolve(id)]); + }); + }; + + const getSessionId = (): string | null => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.getSessionId !== 'function') return null; + return zori.getSessionId(); + }; + + const setConsent = (preferences: ConsentPreferences): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.setConsent === 'function') { + return zori.setConsent(preferences); + } else { + zori.push(['setConsent', preferences]); + return true; + } + }; + + const optOut = (): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.optOut === 'function') { + return zori.optOut(); + } else { + zori.push(['optOut']); + return true; + } + }; + + const hasConsent = (): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.hasConsent !== 'function') return true; + return zori.hasConsent(); + }; + + // Load script immediately + if (typeof window !== 'undefined') { + loadScript(); + } + + return { + isInitialized: readonly(isInitialized), + track, + identify, + getVisitorId, + getSessionId, + setConsent, + optOut, + hasConsent, + }; +} + +// Global store instance (optional, for app-wide usage) +let globalStore: ZoriStore | null = null; + +export function initZori(config: ZoriConfig): ZoriStore { + if (!globalStore) { + globalStore = createZoriStore(config); + } + return globalStore; +} + +export function getZori(): ZoriStore { + if (!globalStore) { + throw new Error('Zori not initialized. Call initZori(config) first.'); + } + return globalStore; +} + +// Action: trackClick +export function trackClick( + node: HTMLElement, + options: { eventName?: string; properties?: Record } = {} +) { + const handleClick = async () => { + const store = getZori(); + await store.track(options.eventName || 'click', options.properties || {}); + }; + + node.addEventListener('click', handleClick); + + return { + destroy() { + node.removeEventListener('click', handleClick); + }, + }; +} + +// Helper: usePageView (for components) +export function usePageView(properties?: Record) { + const store = getZori(); + + onMount(() => { + const unsubscribe = store.isInitialized.subscribe((initialized) => { + if (initialized) { + store.track('page_view', { + page_title: document.title, + page_path: window.location.pathname, + page_search: window.location.search, + page_hash: window.location.hash, + ...properties, + }); + unsubscribe(); + } + }); + }); +} + +// Helper: useTrackEvent (for components) +export function useTrackEvent(eventName: string, properties?: Record) { + const store = getZori(); + + onMount(() => { + const unsubscribe = store.isInitialized.subscribe((initialized) => { + if (initialized) { + store.track(eventName, properties); + unsubscribe(); + } + }); + }); +} + +// Helper: useIdentify (for components) +export function useIdentify(userInfo: UserInfo | null) { + const store = getZori(); + + onMount(() => { + if (!userInfo) return; + + const unsubscribe = store.isInitialized.subscribe((initialized) => { + if (initialized) { + store.identify(userInfo); + unsubscribe(); + } + }); + }); +} + +// Export default +export default { + createZoriStore, + initZori, + getZori, + trackClick, + usePageView, + useTrackEvent, + useIdentify, +}; diff --git a/svelte/package.json b/svelte/package.json new file mode 100644 index 0000000..0a02e01 --- /dev/null +++ b/svelte/package.json @@ -0,0 +1,52 @@ +{ + "name": "@zorihq/svelte", + "version": "1.0.0", + "description": "ZoriHQ Analytics for Svelte", + "main": "index.ts", + "svelte": "index.ts", + "types": "index.ts", + "exports": { + ".": { + "import": "./index.ts", + "require": "./index.ts", + "types": "./index.ts", + "svelte": "./index.ts" + }, + "./ZoriProvider.svelte": "./ZoriProvider.svelte" + }, + "files": [ + "index.ts", + "ZoriProvider.svelte", + "README.md", + "package.json" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "svelte", + "sveltekit", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "svelte": ">=3.0.0" + }, + "dependencies": { + "@zorihq/types": "^1.0.0" + }, + "devDependencies": { + "svelte": "^4.0.0", + "typescript": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "svelte" + } +} diff --git a/svelte/pnpm-lock.yaml b/svelte/pnpm-lock.yaml new file mode 100644 index 0000000..cf485d1 --- /dev/null +++ b/svelte/pnpm-lock.yaml @@ -0,0 +1,183 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@zorihq/types': + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + svelte: + specifier: ^4.0.0 + version: 4.2.20 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@zorihq/types@1.0.0': + resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + code-red@1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + svelte@4.2.20: + resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@types/estree@1.0.8': {} + + '@zorihq/types@1.0.0': {} + + acorn@8.15.0: {} + + aria-query@5.3.2: {} + + axobject-query@4.1.0: {} + + code-red@1.0.4: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@types/estree': 1.0.8 + acorn: 8.15.0 + estree-walker: 3.0.3 + periscopic: 3.1.0 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mdn-data@2.0.30: {} + + periscopic@3.1.0: + dependencies: + '@types/estree': 1.0.8 + estree-walker: 3.0.3 + is-reference: 3.0.3 + + source-map-js@1.2.1: {} + + svelte@4.2.20: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + periscopic: 3.1.0 + + typescript@5.9.3: {} diff --git a/svelte/tsconfig.json b/svelte/tsconfig.json new file mode 100644 index 0000000..23585ec --- /dev/null +++ b/svelte/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.ts"], + "exclude": ["node_modules", "dist", "*.svelte"] +} diff --git a/vue/README.md b/vue/README.md new file mode 100644 index 0000000..71b97c7 --- /dev/null +++ b/vue/README.md @@ -0,0 +1,274 @@ +# @zorihq/vue + +Vue 3 composables and plugin for ZoriHQ Analytics. + +## Installation + +```bash +npm install @zorihq/vue +# or +pnpm add @zorihq/vue +# or +yarn add @zorihq/vue +``` + +## Usage + +### 1. Install the Plugin + +```typescript +// main.ts +import { createApp } from 'vue'; +import { ZoriPlugin } from '@zorihq/vue'; +import App from './App.vue'; +import router from './router'; // Optional: for auto-tracking + +const app = createApp(App); + +app.use(ZoriPlugin, { + config: { + publishableKey: 'your-publishable-key', + baseUrl: 'https://ingestion.zorihq.com/ingest', // optional + comebackThreshold: 30000, // optional + trackQuickSwitches: false, // optional + }, + router, // Optional: pass Vue Router for auto page view tracking + autoTrackPageViews: true, // Optional: default true +}); + +app.use(router); +app.mount('#app'); +``` + +### 2. Use the Composables + +#### useZori - Main Composable + +```vue + + + +``` + +#### usePageView - Auto Track Page Views + +```vue + + + +``` + +#### useIdentify - Auto Identify Users + +```vue + + + +``` + +#### useTrackEvent - Track Events with Reactivity + +```vue + + + +``` + +### 3. Template Usage with v-on + +```vue + + + +``` + +## Vue Router Integration + +When you pass the Vue Router instance to the plugin, it automatically tracks page views on route changes: + +```typescript +app.use(ZoriPlugin, { + config: { publishableKey: 'your-key' }, + router, // Auto-tracks page views + autoTrackPageViews: true, // Enable/disable auto-tracking +}); +``` + +Each route change will track: + +- `page_title`: Document title +- `page_path`: Route path +- `page_name`: Route name +- `page_search`: Query parameters (JSON stringified) + +## API Reference + +### ZoriPlugin Options + +```typescript +{ + config: { + publishableKey: string; // required + baseUrl?: string; // optional + comebackThreshold?: number; // optional + trackQuickSwitches?: boolean; // optional + }, + router?: Router; // optional Vue Router instance + autoTrackPageViews?: boolean; // optional, default true +} +``` + +### useZori() + +Returns an object with: + +- `isInitialized`: Readonly ref indicating if ZoriHQ is ready +- `track(eventName, properties)`: Track custom events +- `identify(userInfo)`: Identify users +- `getVisitorId()`: Get the visitor ID +- `getSessionId()`: Get the current session ID +- `setConsent(preferences)`: Set GDPR consent preferences +- `optOut()`: Opt out of tracking completely +- `hasConsent()`: Check if user has given consent + +### usePageView(properties?) + +Auto-track page view on component mount. Accepts static properties or reactive refs. + +### useTrackEvent(eventName, properties?) + +Track event on component mount. Supports reactive refs for both event name and properties. + +### useIdentify(userInfo) + +Identify user on component mount. Supports reactive refs. + +## TypeScript Support + +This package includes full TypeScript support: + +```typescript +import type { + ZoriConfig, + ZoriInstance, + ConsentPreferences, + UserInfo, +} from '@zorihq/vue'; +``` + +## Composition API + +All composables follow Vue 3 Composition API conventions and support: + +- Reactive refs +- Computed properties +- Automatic cleanup on unmount + +## Options API Support + +You can also use the plugin with Options API: + +```vue + +``` + +## License + +MIT diff --git a/vue/index.ts b/vue/index.ts new file mode 100644 index 0000000..91a0d54 --- /dev/null +++ b/vue/index.ts @@ -0,0 +1,302 @@ +import { + ref, + readonly, + onMounted, + onUnmounted, + watch, + inject, + provide, + type App, + type Ref, + type InjectionKey, +} from "vue"; +import type { + ZoriConfig, + ConsentPreferences, + UserInfo, + ZoriCoreAPI, +} from "@zorihq/types"; + +export type { ZoriConfig, ConsentPreferences, UserInfo } from "@zorihq/types"; + +export interface ZoriInstance extends Omit { + isInitialized: Readonly>; + getSessionId: () => string | null; +} + +export const ZoriKey: InjectionKey = Symbol("zori"); + +export interface ZoriPluginOptions { + config: ZoriConfig; + router?: any; + autoTrackPageViews?: boolean; +} + +function createZoriInstance(config: ZoriConfig): ZoriInstance { + const isInitialized = ref(false); + let scriptLoaded = false; + + const loadScript = () => { + if (scriptLoaded || typeof window === "undefined") return; + + (window as any).ZoriHQ = (window as any).ZoriHQ || []; + + const script = document.createElement("script"); + script.src = "https://cdn.zorihq.com/script.min.js"; + script.async = true; + script.setAttribute("data-key", config.publishableKey); + + if (config.baseUrl) { + script.setAttribute("data-base-url", config.baseUrl); + } + + if (config.comebackThreshold !== undefined) { + script.setAttribute( + "data-comeback-threshold", + config.comebackThreshold.toString(), + ); + } + + if (config.trackQuickSwitches !== undefined) { + script.setAttribute( + "data-track-quick-switches", + config.trackQuickSwitches.toString(), + ); + } + + script.onload = () => { + isInitialized.value = true; + }; + + document.head.appendChild(script); + scriptLoaded = true; + }; + + const track = async ( + eventName: string, + properties?: Record, + ): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.track === "function") { + return await zori.track(eventName, properties); + } else { + zori.push(["track", eventName, properties]); + return true; + } + }; + + const identify = async (userInfo: UserInfo): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.identify === "function") { + return await zori.identify(userInfo); + } else { + zori.push(["identify", userInfo]); + return true; + } + }; + + const getVisitorId = async (): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return ""; + + if (typeof zori.getVisitorId === "function") { + return await zori.getVisitorId(); + } + + return new Promise((resolve) => { + zori.push(["getVisitorId", (id: string) => resolve(id)]); + }); + }; + + const getSessionId = (): string | null => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.getSessionId !== "function") return null; + return zori.getSessionId(); + }; + + const setConsent = (preferences: ConsentPreferences): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.setConsent === "function") { + return zori.setConsent(preferences); + } else { + zori.push(["setConsent", preferences]); + return true; + } + }; + + const optOut = (): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.optOut === "function") { + return zori.optOut(); + } else { + zori.push(["optOut"]); + return true; + } + }; + + const hasConsent = (): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.hasConsent !== "function") return true; + return zori.hasConsent(); + }; + + if (typeof window !== "undefined") { + loadScript(); + } + + return { + isInitialized: readonly(isInitialized), + track, + identify, + getVisitorId, + getSessionId, + setConsent, + optOut, + hasConsent, + }; +} + +export const ZoriPlugin = { + install(app: App, options: ZoriPluginOptions) { + const zoriInstance = createZoriInstance(options.config); + app.provide(ZoriKey, zoriInstance); + + if (options.router && options.autoTrackPageViews !== false) { + options.router.afterEach((to: any) => { + if (zoriInstance.isInitialized.value) { + zoriInstance.track("page_view", { + page_title: document.title, + page_path: to.path, + page_name: to.name, + page_search: to.query ? JSON.stringify(to.query) : "", + }); + } + }); + } + }, +}; + +export function useZori(): ZoriInstance { + const zori = inject(ZoriKey); + if (!zori) { + throw new Error( + "useZori must be used within a component with ZoriPlugin installed", + ); + } + return zori; +} + +export function usePageView( + properties?: Ref> | Record, +) { + const { track, isInitialized } = useZori(); + + onMounted(() => { + if (isInitialized.value) { + const props = + typeof properties === "object" && "value" in properties + ? properties.value + : properties; + track("page_view", { + page_title: document.title, + page_path: window.location.pathname, + page_search: window.location.search, + page_hash: window.location.hash, + ...props, + }); + } + }); + + if (properties && typeof properties === "object" && "value" in properties) { + watch( + properties, + (newProps) => { + if (isInitialized.value) { + track("page_view", { + page_title: document.title, + page_path: window.location.pathname, + page_search: window.location.search, + page_hash: window.location.hash, + ...newProps, + }); + } + }, + { deep: true }, + ); + } +} + +export function useTrackEvent( + eventName: string | Ref, + properties?: Ref> | Record, +) { + const { track, isInitialized } = useZori(); + + onMounted(() => { + if (isInitialized.value) { + const name = typeof eventName === "string" ? eventName : eventName.value; + const props = + typeof properties === "object" && "value" in properties + ? properties.value + : properties; + track(name, props); + } + }); + + watch( + [ + typeof eventName === "string" ? ref(eventName) : eventName, + typeof properties === "object" && "value" in properties + ? properties + : ref(properties), + ], + ([newName, newProps]) => { + if (isInitialized.value) { + track(newName as string, newProps as Record); + } + }, + { deep: true }, + ); +} + +export function useIdentify(userInfo: Ref | UserInfo) { + const { identify, isInitialized } = useZori(); + + onMounted(() => { + const info = + typeof userInfo === "object" && "value" in userInfo + ? userInfo.value + : userInfo; + if (isInitialized.value && info) { + identify(info); + } + }); + + if (userInfo && typeof userInfo === "object" && "value" in userInfo) { + watch( + userInfo, + (newInfo) => { + if (isInitialized.value && newInfo) { + identify(newInfo); + } + }, + { deep: true }, + ); + } +} + +export default { + ZoriPlugin, + useZori, + usePageView, + useTrackEvent, + useIdentify, +}; diff --git a/vue/package.json b/vue/package.json new file mode 100644 index 0000000..421401c --- /dev/null +++ b/vue/package.json @@ -0,0 +1,48 @@ +{ + "name": "@zorihq/vue", + "version": "1.0.0", + "description": "ZoriHQ Analytics for Vue.js", + "main": "index.ts", + "types": "index.ts", + "exports": { + ".": { + "import": "./index.ts", + "require": "./index.ts", + "types": "./index.ts" + } + }, + "files": [ + "index.ts", + "README.md", + "package.json" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "vue", + "vue3", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "vue": ">=3.0.0" + }, + "dependencies": { + "@zorihq/types": "^1.0.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "vue": "^3.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "vue" + } +} diff --git a/vue/pnpm-lock.yaml b/vue/pnpm-lock.yaml new file mode 100644 index 0000000..2a620df --- /dev/null +++ b/vue/pnpm-lock.yaml @@ -0,0 +1,223 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@zorihq/types': + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + typescript: + specifier: ^5.0.0 + version: 5.9.3 + vue: + specifier: ^3.0.0 + version: 3.5.24(typescript@5.9.3) + +packages: + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@vue/compiler-core@3.5.24': + resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} + + '@vue/compiler-dom@3.5.24': + resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==} + + '@vue/compiler-sfc@3.5.24': + resolution: {integrity: sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==} + + '@vue/compiler-ssr@3.5.24': + resolution: {integrity: sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==} + + '@vue/reactivity@3.5.24': + resolution: {integrity: sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==} + + '@vue/runtime-core@3.5.24': + resolution: {integrity: sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==} + + '@vue/runtime-dom@3.5.24': + resolution: {integrity: sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==} + + '@vue/server-renderer@3.5.24': + resolution: {integrity: sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==} + peerDependencies: + vue: 3.5.24 + + '@vue/shared@3.5.24': + resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==} + + '@zorihq/types@1.0.0': + resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + vue@3.5.24: + resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@vue/compiler-core@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.24 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.24': + dependencies: + '@vue/compiler-core': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/compiler-sfc@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.24 + '@vue/compiler-dom': 3.5.24 + '@vue/compiler-ssr': 3.5.24 + '@vue/shared': 3.5.24 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.24': + dependencies: + '@vue/compiler-dom': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/reactivity@3.5.24': + dependencies: + '@vue/shared': 3.5.24 + + '@vue/runtime-core@3.5.24': + dependencies: + '@vue/reactivity': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/runtime-dom@3.5.24': + dependencies: + '@vue/reactivity': 3.5.24 + '@vue/runtime-core': 3.5.24 + '@vue/shared': 3.5.24 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.24 + '@vue/shared': 3.5.24 + vue: 3.5.24(typescript@5.9.3) + + '@vue/shared@3.5.24': {} + + '@zorihq/types@1.0.0': {} + + csstype@3.2.3: {} + + entities@4.5.0: {} + + estree-walker@2.0.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + nanoid@3.3.11: {} + + picocolors@1.1.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + source-map-js@1.2.1: {} + + typescript@5.9.3: {} + + vue@3.5.24(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.24 + '@vue/compiler-sfc': 3.5.24 + '@vue/runtime-dom': 3.5.24 + '@vue/server-renderer': 3.5.24(vue@3.5.24(typescript@5.9.3)) + '@vue/shared': 3.5.24 + optionalDependencies: + typescript: 5.9.3 diff --git a/vue/tsconfig.json b/vue/tsconfig.json new file mode 100644 index 0000000..3517570 --- /dev/null +++ b/vue/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.ts"], + "exclude": ["node_modules", "dist"] +}