chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:54 +08:00
commit 38e12526f7
1648 changed files with 490801 additions and 0 deletions
+41
View File
@@ -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
+149
View File
@@ -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
+16
View File
@@ -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;
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+32
View File
@@ -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"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+373
View File
@@ -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;
}
+106
View File
@@ -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>
);
}
+946
View File
@@ -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>
);
}
+18
View File
@@ -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;
+27
View File
@@ -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"]
}