chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,149 @@
|
||||
# 🚀 Tianji Event Playground
|
||||
|
||||
A beautifully designed interactive playground for testing and exploring Tianji analytics platform capabilities.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🎨 Modern Design
|
||||
- **Glassmorphism UI** - Beautiful glass-morphism cards with backdrop blur effects
|
||||
- **Gradient Backgrounds** - Dynamic gradient backgrounds that adapt to dark/light themes
|
||||
- **Animated Elements** - Floating background shapes and smooth transitions
|
||||
- **Responsive Design** - Fully responsive layout that works on all devices
|
||||
|
||||
### 🔧 Functional Features
|
||||
- **Tracker Initialization** - Easy setup with backend URL and website ID
|
||||
- **Multiple Event Types** - Support for various event types (string, number, date, object, array)
|
||||
- **Session Identification** - User session tracking with generated fake user data
|
||||
- **Real-time Feedback** - Toast notifications with detailed success/error messages
|
||||
- **Event Tracking** - Counter for sent events with visual indicators
|
||||
|
||||
### 🛠️ Technical Stack
|
||||
- **Next.js 15** - React framework with App Router
|
||||
- **TypeScript** - Full type safety
|
||||
- **Tailwind CSS** - Utility-first CSS framework
|
||||
- **Radix UI** - Accessible component primitives
|
||||
- **Sonner** - Beautiful toast notifications
|
||||
- **Tianji Client React** - Official Tianji tracking library
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+
|
||||
- A running Tianji backend instance
|
||||
- Website ID from your Tianji dashboard
|
||||
|
||||
### Installation
|
||||
|
||||
1. Navigate to the project directory:
|
||||
```bash
|
||||
cd example/web
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. Open [http://localhost:7788](http://localhost:7788) in your browser
|
||||
|
||||
### Configuration
|
||||
|
||||
1. **Backend URL**: Enter your Tianji backend URL (e.g., `http://localhost:12345`)
|
||||
2. **Website ID**: Enter your website ID from Tianji dashboard
|
||||
3. Click **"Initialize Tianji Tracker"** to start tracking
|
||||
|
||||
## 🎯 Usage
|
||||
|
||||
### Event Types
|
||||
|
||||
1. **Basic Event** - Simple event without parameters
|
||||
2. **Identify Session** - Associates user data with the current session
|
||||
3. **String Event** - Event with string parameter
|
||||
4. **Number Event** - Event with random number parameter
|
||||
5. **Date Event** - Event with current date/time
|
||||
6. **Object Event** - Event with complex nested object
|
||||
7. **Array Event** - Event with array of numbers
|
||||
|
||||
### Visual Feedback
|
||||
|
||||
- **Status Indicator** - Green pulsing dot when tracker is active
|
||||
- **Event Counter** - Shows total events sent
|
||||
- **Individual Counters** - Badge on each event button showing count
|
||||
- **Toast Messages** - Success/error notifications with details
|
||||
- **Loading States** - Smooth loading animations during operations
|
||||
|
||||
## 🎨 Design Features
|
||||
|
||||
### Color Scheme
|
||||
- **Primary**: Indigo gradients for main actions
|
||||
- **Secondary**: Emerald gradients for events
|
||||
- **Accent**: Amber gradients for special actions
|
||||
- **Background**: Purple-blue gradients with floating elements
|
||||
|
||||
### Animations
|
||||
- **Fade In Up** - Staggered entrance animations
|
||||
- **Hover Effects** - Scale and glow effects on interactive elements
|
||||
- **Floating Shapes** - Animated background elements
|
||||
- **Pulse Animation** - Status indicator breathing effect
|
||||
|
||||
### Responsive Breakpoints
|
||||
- **Mobile**: Single column layout
|
||||
- **Tablet**: Two column event grid
|
||||
- **Desktop**: Three column event grid with enhanced spacing
|
||||
|
||||
## 🛡️ Error Handling
|
||||
|
||||
- **Configuration Validation** - Checks for required fields
|
||||
- **Network Error Handling** - Graceful handling of connection issues
|
||||
- **User Feedback** - Clear error messages with resolution hints
|
||||
- **Loading States** - Prevents multiple concurrent initialization attempts
|
||||
|
||||
## 🔧 Customization
|
||||
|
||||
### Styling
|
||||
All styles are defined in `src/app/globals.css` using CSS custom properties:
|
||||
- `--primary`, `--secondary`, `--accent` for color themes
|
||||
- `--card-background`, `--card-border` for glassmorphism effects
|
||||
- Animation keyframes for custom animations
|
||||
|
||||
### Event Handlers
|
||||
Event configurations are defined in the `eventButtons` array in `page.tsx`:
|
||||
- Easy to add new event types
|
||||
- Configurable descriptions and emojis
|
||||
- Custom success messages
|
||||
|
||||
## 📱 Browser Support
|
||||
|
||||
- Chrome 90+
|
||||
- Firefox 88+
|
||||
- Safari 14+
|
||||
- Edge 90+
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is part of the Tianji analytics platform. See the main repository for license information.
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
If you encounter any issues:
|
||||
1. Check your Tianji backend is running and accessible
|
||||
2. Verify your website ID is correct
|
||||
3. Check browser console for error messages
|
||||
4. Open an issue in the main Tianji repository
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ for the Tianji Analytics Platform
|
||||
@@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "example-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "PORT=7788 next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^8.4.0",
|
||||
"@radix-ui/themes": "^3.2.0",
|
||||
"ahooks": "^3.7.10",
|
||||
"next": "15.1.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sonner": "^1.4.3",
|
||||
"tianji-client-react": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18.2.22",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,373 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--foreground: #ffffff;
|
||||
--card-background: rgba(255, 255, 255, 0.1);
|
||||
--card-border: rgba(255, 255, 255, 0.2);
|
||||
--shadow: rgba(0, 0, 0, 0.1);
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--secondary: #059669;
|
||||
--secondary-hover: #047857;
|
||||
--accent: #dc2626;
|
||||
--accent-hover: #b91c1c;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
--foreground: #ffffff;
|
||||
--card-background: rgba(255, 255, 255, 0.08);
|
||||
--card-border: rgba(255, 255, 255, 0.12);
|
||||
--shadow: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Glassmorphism card */
|
||||
.glass-card {
|
||||
background: var(--card-background);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px var(--shadow);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
box-shadow: 0 12px 48px var(--shadow);
|
||||
}
|
||||
|
||||
/* Animated background */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(180deg); }
|
||||
}
|
||||
|
||||
.floating-shape {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.floating-shape:nth-child(1) {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
top: 20%;
|
||||
left: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.floating-shape:nth-child(2) {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
top: 60%;
|
||||
right: 15%;
|
||||
animation-delay: -2s;
|
||||
}
|
||||
|
||||
.floating-shape:nth-child(3) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
bottom: 20%;
|
||||
left: 20%;
|
||||
animation-delay: -4s;
|
||||
}
|
||||
|
||||
.floating-shape:nth-child(4) {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 30%;
|
||||
right: 30%;
|
||||
animation-delay: -1s;
|
||||
}
|
||||
|
||||
/* Button animations - Target Radix UI button correctly */
|
||||
button.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover)) !important;
|
||||
color: white !important;
|
||||
font-weight: 600 !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 4px 14px 0 rgba(37, 99, 235, 0.39) !important;
|
||||
transition: all 0.3s ease;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
z-index: 1;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
button.btn-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transition: left 0.6s ease-in-out;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
will-change: left;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
button.btn-primary:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
button.btn-secondary {
|
||||
background: linear-gradient(135deg, var(--secondary), var(--secondary-hover)) !important;
|
||||
color: white !important;
|
||||
font-weight: 500 !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 4px 14px 0 rgba(5, 150, 105, 0.35) !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
button.btn-secondary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.6s ease-in-out;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
will-change: left;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
button.btn-secondary:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
button.btn-accent {
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-hover)) !important;
|
||||
color: white !important;
|
||||
font-weight: 500 !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 4px 14px 0 rgba(220, 38, 38, 0.35) !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
button.btn-accent::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.25), transparent);
|
||||
transition: left 0.6s ease-in-out;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
will-change: left;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
button.btn-accent:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* Status indicator */
|
||||
.status-indicator {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--secondary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-dot::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--secondary);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Form field enhancements */
|
||||
.form-field {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-field:focus-within {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Grid animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.animate-delay-100 {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.animate-delay-200 {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.animate-delay-300 {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
/* Fix button animation flickering */
|
||||
button.btn-primary,
|
||||
button.btn-secondary,
|
||||
button.btn-accent {
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
button.btn-primary::before,
|
||||
button.btn-secondary::before,
|
||||
button.btn-accent::before {
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Prevent animation restart on rapid hover */
|
||||
button.btn-primary:not(:hover)::before,
|
||||
button.btn-secondary:not(:hover)::before,
|
||||
button.btn-accent:not(:hover)::before {
|
||||
transition: left 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Enhanced hover states */
|
||||
button.btn-primary:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 8px 25px 0 rgba(37, 99, 235, 0.5) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
button.btn-secondary:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 8px 25px 0 rgba(5, 150, 105, 0.45) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
button.btn-accent:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 8px 25px 0 rgba(220, 38, 38, 0.45) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
/* Additional containment for Radix UI buttons */
|
||||
button.btn-primary > span,
|
||||
button.btn-secondary > span,
|
||||
button.btn-accent > span {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Active states for better feedback */
|
||||
button.btn-primary:active {
|
||||
transform: translateY(0px) !important;
|
||||
box-shadow: 0 2px 8px 0 rgba(37, 99, 235, 0.4) !important;
|
||||
}
|
||||
|
||||
button.btn-secondary:active {
|
||||
transform: translateY(0px) !important;
|
||||
box-shadow: 0 2px 8px 0 rgba(5, 150, 105, 0.4) !important;
|
||||
}
|
||||
|
||||
button.btn-accent:active {
|
||||
transform: translateY(0px) !important;
|
||||
box-shadow: 0 2px 8px 0 rgba(220, 38, 38, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Disabled state improvements */
|
||||
button.btn-primary:disabled,
|
||||
button.btn-secondary:disabled,
|
||||
button.btn-accent:disabled {
|
||||
opacity: 0.6 !important;
|
||||
cursor: not-allowed !important;
|
||||
transform: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter, Geist_Mono } from 'next/font/google';
|
||||
import { Toaster } from 'sonner';
|
||||
import { Theme } from '@radix-ui/themes';
|
||||
import './globals.css';
|
||||
import '@radix-ui/themes/styles.css';
|
||||
|
||||
const inter = Inter({
|
||||
variable: '--font-inter',
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: '--font-geist-mono',
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Tianji Event Playground - Test Your Analytics',
|
||||
description:
|
||||
'Interactive playground to test and explore Tianji tracking capabilities. Send events, identify sessions, and monitor real-time analytics data.',
|
||||
keywords: [
|
||||
'Tianji',
|
||||
'Analytics',
|
||||
'Event Tracking',
|
||||
'Playground',
|
||||
'Monitoring',
|
||||
],
|
||||
authors: [{ name: 'Tianji Team' }],
|
||||
creator: 'Tianji',
|
||||
publisher: 'Tianji',
|
||||
formatDetection: {
|
||||
email: false,
|
||||
address: false,
|
||||
telephone: false,
|
||||
},
|
||||
metadataBase: new URL('http://localhost:7788'),
|
||||
openGraph: {
|
||||
title: 'Tianji Event Playground',
|
||||
description:
|
||||
'Interactive playground for testing Tianji analytics and event tracking',
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-video-preview': -1,
|
||||
'max-image-preview': 'large',
|
||||
'max-snippet': -1,
|
||||
},
|
||||
},
|
||||
viewport: {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${geistMono.variable}`}>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin=""
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans antialiased">
|
||||
<Theme
|
||||
appearance="dark"
|
||||
accentColor="violet"
|
||||
grayColor="slate"
|
||||
scaling="100%"
|
||||
>
|
||||
{children}
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
expand={true}
|
||||
richColors
|
||||
closeButton
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Theme>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,946 @@
|
||||
'use client';
|
||||
import { useLocalStorageState } from 'ahooks';
|
||||
import {
|
||||
initTianjiTracker,
|
||||
reportWebsiteEvent,
|
||||
identifyWebsiteUser,
|
||||
} from 'tianji-client-react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button, Code, TextField } from '@radix-ui/themes';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
// 定义 tianji tracker 的类型
|
||||
interface TianjiPayload {
|
||||
website: string;
|
||||
hostname: string;
|
||||
screen: string;
|
||||
language: string;
|
||||
title: string;
|
||||
url: string;
|
||||
referrer: string;
|
||||
}
|
||||
|
||||
interface TianjiTracker {
|
||||
track: (
|
||||
payload?:
|
||||
| string
|
||||
| TianjiPayload
|
||||
| ((payload: TianjiPayload) => TianjiPayload)
|
||||
) => void;
|
||||
identify: (data: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
tianji?: TianjiTracker;
|
||||
}
|
||||
}
|
||||
interface LoadingSpinnerProps {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) {
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-6 h-6',
|
||||
lg: 'w-8 h-8',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inline-block animate-spin rounded-full border-2 border-solid border-current border-r-transparent ${sizeClasses[size]} ${className}`}
|
||||
>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [backendUrl, setBackendUrl] = useLocalStorageState(
|
||||
'tianji.backendUrl',
|
||||
{
|
||||
defaultValue: 'http://localhost:12345',
|
||||
}
|
||||
);
|
||||
const [websiteId, setWebsiteId] = useLocalStorageState('tianji.websiteId', {
|
||||
defaultValue: '',
|
||||
});
|
||||
const [injectedEl, setInjectedEl] = useState<HTMLScriptElement | null>(null);
|
||||
const [isInjecting, setIsInjecting] = useState(false);
|
||||
const [eventCounts, setEventCounts] = useState<Record<string, number>>({});
|
||||
const [isBatchSending, setIsBatchSending] = useState(false);
|
||||
const [batchProgress, setBatchProgress] = useState(0);
|
||||
|
||||
const injectTracker = async () => {
|
||||
if (isInjecting) return;
|
||||
|
||||
setIsInjecting(true);
|
||||
|
||||
if (injectedEl) {
|
||||
injectedEl.remove();
|
||||
setInjectedEl(null);
|
||||
setEventCounts({});
|
||||
}
|
||||
|
||||
if (!backendUrl || !websiteId) {
|
||||
toast.error(
|
||||
'Missing required fields! Please fill in both Backend URL and Website ID.',
|
||||
{
|
||||
description: 'Both fields are required to initialize the tracker.',
|
||||
}
|
||||
);
|
||||
setIsInjecting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const scriptEl = await initTianjiTracker({
|
||||
url: backendUrl,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
console.log('scriptEl', scriptEl);
|
||||
if (scriptEl) {
|
||||
setInjectedEl(scriptEl);
|
||||
}
|
||||
toast.success('Tianji tracker injected successfully!', {
|
||||
description:
|
||||
'You can now start sending events to your analytics dashboard.',
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
'Failed to inject tracker. Please check your configuration.',
|
||||
{
|
||||
description: 'Make sure your backend URL is correct and accessible.',
|
||||
}
|
||||
);
|
||||
console.error('Tracker injection error:', error);
|
||||
} finally {
|
||||
setIsInjecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const incrementEventCount = (eventType: string, count: number = 1) => {
|
||||
setEventCounts((prev) => ({
|
||||
...prev,
|
||||
[eventType]: (prev[eventType] || 0) + count,
|
||||
}));
|
||||
};
|
||||
|
||||
// 封装 pageview 发送函数
|
||||
const sendPageView = (
|
||||
pageUrl: string,
|
||||
pageTitle: string,
|
||||
pageReferrer: string
|
||||
) => {
|
||||
if (window.tianji && websiteId) {
|
||||
window.tianji.track({
|
||||
website: websiteId,
|
||||
hostname: window.location.hostname,
|
||||
screen: `${window.screen.width}x${window.screen.height}`,
|
||||
language: navigator.language,
|
||||
title: pageTitle,
|
||||
url: pageUrl,
|
||||
referrer: pageReferrer,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const batchSendEvents = async (eventType: string, eventTitle: string) => {
|
||||
if (isBatchSending) return;
|
||||
|
||||
setIsBatchSending(true);
|
||||
setBatchProgress(0);
|
||||
|
||||
const batchSize = 100;
|
||||
const chunkSize = 10; // Send 10 events at a time to avoid overwhelming
|
||||
|
||||
try {
|
||||
for (let i = 0; i < batchSize; i += chunkSize) {
|
||||
const currentChunk = Math.min(chunkSize, batchSize - i);
|
||||
|
||||
// Send a chunk of events
|
||||
for (let j = 0; j < currentChunk; j++) {
|
||||
switch (eventType) {
|
||||
case 'basic':
|
||||
reportWebsiteEvent('Tianji Demo Event (Batch)');
|
||||
break;
|
||||
case 'pageview':
|
||||
const pages = [
|
||||
'/home',
|
||||
'/about',
|
||||
'/products',
|
||||
'/blog',
|
||||
'/contact',
|
||||
'/pricing',
|
||||
'/features',
|
||||
'/docs',
|
||||
'/support',
|
||||
'/login',
|
||||
'/signup',
|
||||
'/dashboard',
|
||||
'/profile',
|
||||
'/settings',
|
||||
'/search',
|
||||
'/api',
|
||||
'/help',
|
||||
'/terms',
|
||||
'/privacy',
|
||||
'/news',
|
||||
'/events',
|
||||
'/careers',
|
||||
'/team',
|
||||
'/partners',
|
||||
];
|
||||
const referrers = [
|
||||
'https://google.com',
|
||||
'https://twitter.com',
|
||||
'https://facebook.com',
|
||||
'https://linkedin.com',
|
||||
'https://github.com',
|
||||
'direct',
|
||||
'https://reddit.com',
|
||||
'https://youtube.com',
|
||||
'https://bing.com',
|
||||
'https://duckduckgo.com',
|
||||
'https://yahoo.com',
|
||||
];
|
||||
const randomPage =
|
||||
pages[Math.floor(Math.random() * pages.length)];
|
||||
const randomReferrer =
|
||||
referrers[Math.floor(Math.random() * referrers.length)];
|
||||
|
||||
sendPageView(
|
||||
randomPage,
|
||||
`${randomPage.slice(1) || 'Home'} - Tianji Demo`,
|
||||
randomReferrer
|
||||
);
|
||||
break;
|
||||
case 'unique_visitor':
|
||||
const userId = faker.string.uuid();
|
||||
const sessionId = faker.string.uuid();
|
||||
const userAgent = faker.internet.userAgent();
|
||||
const country = faker.location.country();
|
||||
const city = faker.location.city();
|
||||
|
||||
identifyWebsiteUser({
|
||||
userId,
|
||||
sessionId,
|
||||
username: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
country,
|
||||
city,
|
||||
userAgent,
|
||||
firstVisit: Math.random() > 0.8, // 20% chance of being first visit
|
||||
timestamp: Date.now() - Math.random() * 604800000, // Random time within last week
|
||||
batchId: i + j,
|
||||
});
|
||||
break;
|
||||
case 'string':
|
||||
reportWebsiteEvent('Tianji String Event (Batch)', {
|
||||
string: `batch-${i + j}`,
|
||||
});
|
||||
break;
|
||||
case 'number':
|
||||
reportWebsiteEvent('Tianji Number Event (Batch)', {
|
||||
number: Math.round(Math.random() * 1000),
|
||||
});
|
||||
break;
|
||||
case 'date':
|
||||
reportWebsiteEvent('Tianji Date Event (Batch)', {
|
||||
date: new Date(),
|
||||
});
|
||||
break;
|
||||
case 'object':
|
||||
reportWebsiteEvent('Tianji Object Event (Batch)', {
|
||||
object: {
|
||||
batchId: i + j,
|
||||
timestamp: Date.now(),
|
||||
data: 'batch-data',
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'array':
|
||||
reportWebsiteEvent('Tianji Array Event (Batch)', {
|
||||
array: [i + j, 'batch', Math.random()],
|
||||
});
|
||||
break;
|
||||
case 'identify':
|
||||
identifyWebsiteUser({
|
||||
username: `BatchUser-${i + j}`,
|
||||
email: `batch${i + j}@example.com`,
|
||||
batchId: i + j,
|
||||
});
|
||||
break;
|
||||
case 'user_session':
|
||||
// Create a user session with multiple page views
|
||||
const sessionUserId = faker.string.uuid();
|
||||
const sessionSessionId = faker.string.uuid();
|
||||
const sessionUserAgent = faker.internet.userAgent();
|
||||
const sessionCountry = faker.location.country();
|
||||
const sessionCity = faker.location.city();
|
||||
|
||||
identifyWebsiteUser({
|
||||
userId: sessionUserId,
|
||||
sessionId: sessionSessionId,
|
||||
username: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
country: sessionCountry,
|
||||
city: sessionCity,
|
||||
userAgent: sessionUserAgent,
|
||||
firstVisit: Math.random() > 0.6,
|
||||
batchId: i + j,
|
||||
});
|
||||
|
||||
// Send 2-3 page views for this session
|
||||
const sessionPages = ['/home', '/about', '/products', '/contact'];
|
||||
const numSessionPages = Math.floor(Math.random() * 2) + 2;
|
||||
|
||||
for (let k = 0; k < numSessionPages; k++) {
|
||||
const page = sessionPages[Math.min(k, sessionPages.length - 1)];
|
||||
const referrer =
|
||||
k === 0 ? 'https://google.com' : sessionPages[k - 1];
|
||||
|
||||
sendPageView(
|
||||
page,
|
||||
`${page.slice(1) || 'Home'} - Tianji Demo`,
|
||||
referrer
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'traffic_boost':
|
||||
// Send mixed traffic data
|
||||
const trafficType = Math.random();
|
||||
|
||||
if (trafficType < 0.4) {
|
||||
// Page view
|
||||
const trafficPages = ['/home', '/about', '/products', '/blog'];
|
||||
const trafficReferrers = [
|
||||
'https://google.com',
|
||||
'https://twitter.com',
|
||||
'direct',
|
||||
];
|
||||
const randomTrafficPage =
|
||||
trafficPages[Math.floor(Math.random() * trafficPages.length)];
|
||||
const randomTrafficReferrer =
|
||||
trafficReferrers[
|
||||
Math.floor(Math.random() * trafficReferrers.length)
|
||||
];
|
||||
|
||||
sendPageView(
|
||||
randomTrafficPage,
|
||||
`${randomTrafficPage.slice(1) || 'Home'} - Tianji Demo`,
|
||||
randomTrafficReferrer
|
||||
);
|
||||
} else if (trafficType < 0.6) {
|
||||
// New visitor
|
||||
identifyWebsiteUser({
|
||||
userId: faker.string.uuid(),
|
||||
sessionId: faker.string.uuid(),
|
||||
username: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
country: faker.location.country(),
|
||||
city: faker.location.city(),
|
||||
userAgent: faker.internet.userAgent(),
|
||||
firstVisit: Math.random() > 0.7,
|
||||
});
|
||||
} else {
|
||||
// Custom event
|
||||
const customEvents = [
|
||||
'button_click',
|
||||
'form_submit',
|
||||
'video_play',
|
||||
'download',
|
||||
];
|
||||
const randomCustomEvent =
|
||||
customEvents[Math.floor(Math.random() * customEvents.length)];
|
||||
|
||||
reportWebsiteEvent(randomCustomEvent, {
|
||||
element: `${randomCustomEvent}_${Math.floor(Math.random() * 100)}`,
|
||||
timestamp: Date.now() - Math.random() * 86400000,
|
||||
batchId: i + j,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress and count
|
||||
const newProgress = Math.round(((i + currentChunk) / batchSize) * 100);
|
||||
setBatchProgress(newProgress);
|
||||
incrementEventCount(eventType, currentChunk);
|
||||
|
||||
// Small delay to prevent overwhelming the system
|
||||
if (i + chunkSize < batchSize) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(`Successfully sent 100 ${eventTitle} events!`, {
|
||||
description: `All events have been processed and sent to your analytics dashboard.`,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(`Failed to send batch events`, {
|
||||
description: 'Some events may not have been sent successfully.',
|
||||
});
|
||||
console.error('Batch send error:', error);
|
||||
} finally {
|
||||
setIsBatchSending(false);
|
||||
setBatchProgress(0);
|
||||
}
|
||||
};
|
||||
|
||||
const eventButtons = [
|
||||
{
|
||||
title: 'Send Basic Event',
|
||||
description: 'Send a simple event without parameters',
|
||||
variant: 'secondary' as const,
|
||||
emoji: '🎯',
|
||||
eventType: 'basic',
|
||||
action: () => {
|
||||
reportWebsiteEvent('Tianji Demo Event');
|
||||
incrementEventCount('basic');
|
||||
toast.success('Basic event sent successfully!');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Fake Page View',
|
||||
description: 'Simulate a page view with random URL',
|
||||
variant: 'accent' as const,
|
||||
emoji: '👁️',
|
||||
eventType: 'pageview',
|
||||
action: () => {
|
||||
const pages = [
|
||||
'/home',
|
||||
'/about',
|
||||
'/products',
|
||||
'/blog',
|
||||
'/contact',
|
||||
'/pricing',
|
||||
'/features',
|
||||
'/docs',
|
||||
'/support',
|
||||
'/login',
|
||||
'/signup',
|
||||
'/dashboard',
|
||||
'/profile',
|
||||
'/settings',
|
||||
'/search',
|
||||
];
|
||||
const randomPage = pages[Math.floor(Math.random() * pages.length)];
|
||||
const referrers = [
|
||||
'https://google.com',
|
||||
'https://twitter.com',
|
||||
'https://facebook.com',
|
||||
'https://linkedin.com',
|
||||
'https://github.com',
|
||||
'direct',
|
||||
'https://reddit.com',
|
||||
'https://youtube.com',
|
||||
];
|
||||
const randomReferrer =
|
||||
referrers[Math.floor(Math.random() * referrers.length)];
|
||||
|
||||
// 使用封装的函数发送 pageview
|
||||
sendPageView(
|
||||
randomPage,
|
||||
`${randomPage.slice(1) || 'Home'} - Tianji Demo`,
|
||||
randomReferrer
|
||||
);
|
||||
|
||||
incrementEventCount('pageview');
|
||||
toast.success(`Page view sent: ${randomPage}`, {
|
||||
description: `Referrer: ${randomReferrer}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Fake Unique Visitor',
|
||||
description: 'Create a new unique visitor session',
|
||||
variant: 'accent' as const,
|
||||
emoji: '👤',
|
||||
eventType: 'unique_visitor',
|
||||
action: () => {
|
||||
const userId = faker.string.uuid();
|
||||
const sessionId = faker.string.uuid();
|
||||
const userAgent = faker.internet.userAgent();
|
||||
const country = faker.location.country();
|
||||
const city = faker.location.city();
|
||||
|
||||
identifyWebsiteUser({
|
||||
userId,
|
||||
sessionId,
|
||||
username: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
country,
|
||||
city,
|
||||
userAgent,
|
||||
firstVisit: Math.random() > 0.7, // 30% chance of being first visit
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
incrementEventCount('unique_visitor');
|
||||
toast.success(`New unique visitor created!`, {
|
||||
description: `From ${city}, ${country}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Identify Session',
|
||||
description: 'Identify current session with user info',
|
||||
variant: 'accent' as const,
|
||||
emoji: '👤',
|
||||
eventType: 'identify',
|
||||
action: () => {
|
||||
const info = {
|
||||
username: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
avatar: faker.image.avatar(),
|
||||
};
|
||||
identifyWebsiteUser(info);
|
||||
incrementEventCount('identify');
|
||||
toast.success(`Session identified as: ${info.username}`, {
|
||||
description: `Email: ${info.email}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'String Event',
|
||||
description: 'Send event with string parameter',
|
||||
variant: 'secondary' as const,
|
||||
emoji: '📝',
|
||||
eventType: 'string',
|
||||
action: () => {
|
||||
reportWebsiteEvent('Tianji String Event', { string: 'bar' });
|
||||
incrementEventCount('string');
|
||||
toast.success('String event sent!');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Number Event',
|
||||
description: 'Send event with number parameter',
|
||||
variant: 'secondary' as const,
|
||||
emoji: '🔢',
|
||||
eventType: 'number',
|
||||
action: () => {
|
||||
const number = Math.round(Math.random() * 1000);
|
||||
reportWebsiteEvent('Tianji Number Event', { number });
|
||||
incrementEventCount('number');
|
||||
toast.success(`Number event sent with value: ${number}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Date Event',
|
||||
description: 'Send event with date parameter',
|
||||
variant: 'secondary' as const,
|
||||
emoji: '📅',
|
||||
eventType: 'date',
|
||||
action: () => {
|
||||
const date = new Date();
|
||||
reportWebsiteEvent('Tianji Date Event', { date });
|
||||
incrementEventCount('date');
|
||||
toast.success(`Date event sent: ${date.toLocaleString()}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Object Event',
|
||||
description: 'Send event with complex object',
|
||||
variant: 'secondary' as const,
|
||||
emoji: '🧩',
|
||||
eventType: 'object',
|
||||
action: () => {
|
||||
const obj = { a: 1, b: '2', c: { d: 3 } };
|
||||
reportWebsiteEvent('Tianji Object Event', { object: obj });
|
||||
incrementEventCount('object');
|
||||
toast.success('Object event sent!', {
|
||||
description: 'Complex nested object with multiple data types',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Array Event',
|
||||
description: 'Send event with array parameter',
|
||||
variant: 'secondary' as const,
|
||||
emoji: '📊',
|
||||
eventType: 'array',
|
||||
action: () => {
|
||||
const array = [1, 2, 3, 4, 5];
|
||||
reportWebsiteEvent('Tianji Array Event', { array });
|
||||
incrementEventCount('array');
|
||||
toast.success(`Array event sent: [${array.join(', ')}]`);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Simulate User Session',
|
||||
description: 'Create realistic user session with multiple page views',
|
||||
variant: 'accent' as const,
|
||||
emoji: '🌐',
|
||||
eventType: 'user_session',
|
||||
action: () => {
|
||||
const userId = faker.string.uuid();
|
||||
const sessionId = faker.string.uuid();
|
||||
const userAgent = faker.internet.userAgent();
|
||||
const country = faker.location.country();
|
||||
const city = faker.location.city();
|
||||
const userName = faker.person.fullName();
|
||||
const userEmail = faker.internet.email();
|
||||
|
||||
// First, identify the user
|
||||
identifyWebsiteUser({
|
||||
userId,
|
||||
sessionId,
|
||||
username: userName,
|
||||
email: userEmail,
|
||||
country,
|
||||
city,
|
||||
userAgent,
|
||||
firstVisit: Math.random() > 0.6, // 40% chance of being first visit
|
||||
});
|
||||
|
||||
// Then simulate a browsing session with multiple page views
|
||||
const sessionPages = [
|
||||
'/home',
|
||||
'/about',
|
||||
'/products',
|
||||
'/pricing',
|
||||
'/contact',
|
||||
];
|
||||
const numPages = Math.floor(Math.random() * 4) + 2; // 2-5 pages
|
||||
|
||||
for (let i = 0; i < numPages; i++) {
|
||||
const page = sessionPages[Math.min(i, sessionPages.length - 1)];
|
||||
const referrer = i === 0 ? 'https://google.com' : sessionPages[i - 1];
|
||||
|
||||
setTimeout(() => {
|
||||
sendPageView(
|
||||
page,
|
||||
`${page.slice(1) || 'Home'} - Tianji Demo`,
|
||||
referrer
|
||||
);
|
||||
}, i * 1000); // 1 second delay between page views
|
||||
}
|
||||
|
||||
incrementEventCount('user_session');
|
||||
toast.success(`User session created: ${userName}`, {
|
||||
description: `${numPages} page views from ${city}, ${country}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Analytics Traffic Boost',
|
||||
description: 'Mix of PV, UV, and events to simulate real traffic',
|
||||
variant: 'accent' as const,
|
||||
emoji: '📈',
|
||||
eventType: 'traffic_boost',
|
||||
action: () => {
|
||||
const eventsToSend = Math.floor(Math.random() * 10) + 5; // 5-15 events
|
||||
let sentEvents = 0;
|
||||
|
||||
for (let i = 0; i < eventsToSend; i++) {
|
||||
setTimeout(() => {
|
||||
const eventType = Math.random();
|
||||
|
||||
if (eventType < 0.4) {
|
||||
// 40% chance: Page view
|
||||
const pages = [
|
||||
'/home',
|
||||
'/about',
|
||||
'/products',
|
||||
'/blog',
|
||||
'/contact',
|
||||
];
|
||||
const referrers = [
|
||||
'https://google.com',
|
||||
'https://twitter.com',
|
||||
'direct',
|
||||
];
|
||||
const randomPage =
|
||||
pages[Math.floor(Math.random() * pages.length)];
|
||||
const randomReferrer =
|
||||
referrers[Math.floor(Math.random() * referrers.length)];
|
||||
|
||||
sendPageView(
|
||||
randomPage,
|
||||
`${randomPage.slice(1) || 'Home'} - Tianji Demo`,
|
||||
randomReferrer
|
||||
);
|
||||
} else if (eventType < 0.6) {
|
||||
// 20% chance: New unique visitor
|
||||
identifyWebsiteUser({
|
||||
userId: faker.string.uuid(),
|
||||
sessionId: faker.string.uuid(),
|
||||
username: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
country: faker.location.country(),
|
||||
city: faker.location.city(),
|
||||
userAgent: faker.internet.userAgent(),
|
||||
firstVisit: Math.random() > 0.7,
|
||||
});
|
||||
} else if (eventType < 0.8) {
|
||||
// 20% chance: Custom event
|
||||
const customEvents = [
|
||||
'button_click',
|
||||
'form_submit',
|
||||
'video_play',
|
||||
'download',
|
||||
];
|
||||
const randomEvent =
|
||||
customEvents[Math.floor(Math.random() * customEvents.length)];
|
||||
|
||||
reportWebsiteEvent(randomEvent, {
|
||||
element: `${randomEvent}_${Math.floor(Math.random() * 100)}`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
// 20% chance: User interaction
|
||||
const interactions = ['scroll', 'hover', 'focus', 'resize'];
|
||||
const randomInteraction =
|
||||
interactions[Math.floor(Math.random() * interactions.length)];
|
||||
|
||||
reportWebsiteEvent('user_interaction', {
|
||||
type: randomInteraction,
|
||||
duration: Math.floor(Math.random() * 5000) + 1000,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
sentEvents++;
|
||||
if (sentEvents === eventsToSend) {
|
||||
incrementEventCount('traffic_boost');
|
||||
toast.success(`Traffic boost complete!`, {
|
||||
description: `Sent ${eventsToSend} mixed analytics events`,
|
||||
});
|
||||
}
|
||||
}, i * 200); // 200ms delay between events
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const totalEvents = Object.values(eventCounts).reduce(
|
||||
(sum, count) => sum + count,
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
{/* Animated background shapes */}
|
||||
<div className="floating-shape"></div>
|
||||
<div className="floating-shape"></div>
|
||||
<div className="floating-shape"></div>
|
||||
<div className="floating-shape"></div>
|
||||
|
||||
<div className="relative z-10 flex min-h-screen items-center justify-center p-4">
|
||||
<div className="w-full max-w-4xl">
|
||||
{/* Header */}
|
||||
<div className="animate-fade-in-up mb-12 text-center">
|
||||
<h1 className="mb-4 bg-gradient-to-r from-white via-blue-100 to-purple-100 bg-clip-text text-5xl font-bold text-transparent">
|
||||
🚀 Tianji Event Playground
|
||||
</h1>
|
||||
<p className="mb-2 text-xl text-white/80">
|
||||
Test and explore Tianji tracking capabilities
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-white/60">
|
||||
<span>Real-time event tracking made simple</span>
|
||||
<span className="h-1 w-1 rounded-full bg-white/40"></span>
|
||||
<span>Modern analytics platform</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Section */}
|
||||
<div className="glass-card animate-fade-in-up animate-delay-100 mb-8 p-8">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-r from-blue-500 to-purple-500">
|
||||
<span className="font-bold text-white">⚙️</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-white">
|
||||
Configuration
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="form-field">
|
||||
<label className="mb-2 block text-sm font-medium text-white/90">
|
||||
Backend URL
|
||||
</label>
|
||||
<TextField.Root
|
||||
size="3"
|
||||
variant="surface"
|
||||
placeholder="Enter your Tianji backend URL"
|
||||
value={backendUrl}
|
||||
onChange={(e) => setBackendUrl(e.target.value)}
|
||||
className="w-full"
|
||||
disabled={isInjecting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label className="mb-2 block text-sm font-medium text-white/90">
|
||||
Website ID
|
||||
</label>
|
||||
<TextField.Root
|
||||
size="3"
|
||||
variant="surface"
|
||||
placeholder="Enter your website ID"
|
||||
value={websiteId}
|
||||
onChange={(e) => setWebsiteId(e.target.value)}
|
||||
className="w-full"
|
||||
disabled={isInjecting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="4"
|
||||
className="btn-primary w-full cursor-pointer"
|
||||
onClick={injectTracker}
|
||||
disabled={isInjecting}
|
||||
>
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
{isInjecting ? (
|
||||
<>
|
||||
<LoadingSpinner size="sm" />
|
||||
<span>Initializing Tracker...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>🔌</span>
|
||||
<span>
|
||||
{injectedEl ? 'Reinitialize' : 'Initialize'} Tianji
|
||||
Tracker
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Status Section */}
|
||||
{injectedEl && (
|
||||
<div className="glass-card animate-fade-in-up animate-delay-200 mb-8 p-8">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="status-indicator">
|
||||
<div className="status-dot"></div>
|
||||
<span className="text-lg font-semibold text-white">
|
||||
Tracker Active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{totalEvents > 0 && (
|
||||
<div className="rounded-lg bg-gradient-to-r from-green-500/20 to-blue-500/20 px-4 py-2">
|
||||
<span className="font-semibold text-white">
|
||||
{totalEvents} Events Sent
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6 rounded-lg bg-black/20 p-4">
|
||||
<p className="mb-2 text-white/80">Connected Website ID:</p>
|
||||
<Code size="3" className="bg-white/10 text-white">
|
||||
{injectedEl.getAttribute('data-website-id')}
|
||||
</Code>
|
||||
</div>
|
||||
|
||||
{/* Event Actions */}
|
||||
<div className="space-y-6">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-r from-green-500 to-blue-500">
|
||||
<span className="font-bold text-white">📊</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Event Actions
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Batch Progress Bar */}
|
||||
{isBatchSending && (
|
||||
<div className="mb-4 rounded-lg bg-gradient-to-r from-orange-500/20 to-red-500/20 p-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium text-white">
|
||||
Sending batch events...
|
||||
</span>
|
||||
<span className="text-sm text-white/80">
|
||||
{batchProgress}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-black/20">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-orange-500 to-red-500 transition-all duration-300 ease-out"
|
||||
style={{ width: `${batchProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{eventButtons.map((button, index) => (
|
||||
<div
|
||||
key={button.title}
|
||||
className="glass-card animate-fade-in-up p-6 transition-all duration-300 hover:scale-105"
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-2xl">{button.emoji}</span>
|
||||
<h4 className="font-semibold text-white">
|
||||
{button.title}
|
||||
</h4>
|
||||
{eventCounts[button.eventType] && (
|
||||
<span className="ml-auto rounded-full bg-white/20 px-2 py-1 text-xs text-white">
|
||||
{eventCounts[button.eventType]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-white/70">
|
||||
{button.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="3"
|
||||
variant="soft"
|
||||
className={`!flex-1 cursor-pointer transition-all hover:scale-105 ${
|
||||
button.variant === 'accent'
|
||||
? 'btn-accent'
|
||||
: 'btn-secondary'
|
||||
}`}
|
||||
onClick={button.action}
|
||||
>
|
||||
Send 1x
|
||||
</Button>
|
||||
<Button
|
||||
size="3"
|
||||
variant="soft"
|
||||
className="btn-accent cursor-pointer transition-all hover:scale-105"
|
||||
onClick={() =>
|
||||
batchSendEvents(button.eventType, button.title)
|
||||
}
|
||||
disabled={isBatchSending}
|
||||
>
|
||||
100x
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="animate-fade-in-up animate-delay-300 text-center text-white/60">
|
||||
<p className="mb-2">
|
||||
Built with ❤️ using Tianji Analytics Platform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user