npm package: @spin-studio/vue
Description: Vue SDK for Spin Studio - embeddable prize wheel component
Author: Spin Studio
Latest version: 0.1.1

# @spin-studio/vue

Vue 3 SDK for Spin Studio - embeddable prize wheel components with full TypeScript support.

## Installation

```bash
npm install @spin-studio/vue
```

**Requirements:** Vue 3.3.0 or higher

## Quick Start

### 1. Get Your API Key & Create a Wheel

1. Sign up at [spin-studio.weez.boo](https://spin-studio.weez.boo)
2. Create an organization
3. Go to **Settings → API Keys → Create API Key**
4. Go to **Dashboard → Wheels → Create Wheel**
5. Add segments, customize, and **Publish**

### 2. Add Provider

Wrap your app with `SpinStudioProvider`:

```vue
<!-- App.vue -->
<script setup lang="ts">
import { SpinStudioProvider } from '@spin-studio/vue';

const config = {
  apiKey: 'sk_live_your_api_key_here',
  baseUrl: 'https://spin-studio.weez.boo'
};
</script>

<template>
  <SpinStudioProvider :config="config">
    <RouterView />
  </SpinStudioProvider>
</template>
```

### 3. Use the SpinWheel Component

```vue
<script setup lang="ts">
import { SpinWheel } from '@spin-studio/vue';

const handlePrizeWon = (event) => {
  console.log('Won:', event.prize);
  console.log('Code:', event.redemptionCode);

  // Apply discount to cart
  applyDiscount(event.prize.value);
};
</script>

<template>
  <SpinWheel
    wheel-id="your-wheel-id"
    @prize-won="handlePrizeWon"
  />
</template>
```

## Usage Examples

### Basic Inline Wheel

```vue
<script setup lang="ts">
import { SpinStudioProvider, SpinWheel } from '@spin-studio/vue';

const config = { apiKey: 'sk_live_xxx' };

const handlePrizeWon = (event) => {
  console.log('Won:', event.prize);
};
</script>

<template>
  <SpinStudioProvider :config="config">
    <div style="width: 400px; height: 400px;">
      <SpinWheel
        wheel-id="wheel-id"
        :size="400"
        @prize-won="handlePrizeWon"
      />
    </div>
  </SpinStudioProvider>
</template>
```

### With All Event Handlers

```vue
<script setup lang="ts">
import { SpinWheel } from '@spin-studio/vue';

const onReady = (e) => console.log('Wheel ready');
const onSpinStart = (e) => console.log('Spinning...');
const onSpinEnd = (e) => console.log('Spin ended');

const onPrizeWon = (e) => {
  const { prize, redemptionCode } = e;

  // prize.type: 'discount_percentage' | 'discount_fixed' | 'free_shipping' | ...
  // prize.value: 15 (for 15% off)
  // redemptionCode: 'SPIN-ABC123'

  applyToCart(prize, redemptionCode);
};

const onLevelUp = (e) => console.log(`Level up! ${e.fromLevel} → ${e.toLevel}`);
const onError = (e) => console.error('Error:', e.message);
</script>

<template>
  <SpinWheel
    wheel-id="wheel-id"
    :size="400"
    @ready="onReady"
    @spin-start="onSpinStart"
    @spin-end="onSpinEnd"
    @prize-won="onPrizeWon"
    @level-up="onLevelUp"
    @error="onError"
  />
</template>
```

### Using the Composable

```vue
<script setup lang="ts">
import { useSpinStudio } from '@spin-studio/vue';

const { openWheel, closeWheel, isReady } = useSpinStudio();

const handleClick = async () => {
  await openWheel('wheel-id', {
    mode: 'modal',
    onPrizeWon: (e) => console.log('Won:', e.prize)
  });
};
</script>

<template>
  <button @click="handleClick" :disabled="!isReady">
    🎡 Spin to Win!
  </button>
</template>
```

### With Template Ref for Programmatic Control

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { SpinWheel } from '@spin-studio/vue';
import type { SpinWheelRef } from '@spin-studio/vue';

const wheelRef = ref<SpinWheelRef | null>(null);

const handleSpin = async () => {
  if (wheelRef.value) {
    const result = await wheelRef.value.spin();
    console.log('Result:', result);
  }
};

const handleReset = () => {
  wheelRef.value?.reset();
};
</script>

<template>
  <div>
    <SpinWheel ref="wheelRef" wheel-id="wheel-id" hide-spin-button />
    <button @click="handleSpin">Spin!</button>
    <button @click="handleReset">Reset</button>
  </div>
</template>
```

### Custom Spin Button (Slot)

```vue
<template>
  <SpinWheel wheel-id="wheel-id">
    <template #spin-button>
      <button class="my-custom-button">
        🎰 Try Your Luck!
      </button>
    </template>
  </SpinWheel>
</template>
```

## API Reference

### `SpinStudioProvider`

Context provider for SDK configuration.

```vue
<SpinStudioProvider :config="config">
  <!-- Your app -->
</SpinStudioProvider>
```

```typescript
interface Config {
  apiKey: string;              // Required
  baseUrl?: string;            // Default: 'https://spin-studio.weez.boo'
  debug?: boolean;             // Default: false
}
```

### `SpinWheel`

The wheel component.

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `wheel-id` | `string` | Required | The wheel ID |
| `wheel-data` | `WheelData` | - | Pre-loaded wheel data |
| `size` | `number` | `400` | Wheel size in pixels |
| `spinnable` | `boolean` | `true` | Enable spinning |
| `initial-level` | `number` | `1` | Starting level |
| `hide-spin-button` | `boolean` | `false` | Hide default button |

#### Events

| Event | Payload | Description |
|-------|---------|-------------|
| `@ready` | `ReadyEvent` | Wheel loaded |
| `@spin-start` | `SpinStartEvent` | Spin started |
| `@spin-end` | `SpinEndEvent` | Spin ended |
| `@prize-won` | `PrizeWonEvent` | Prize won |
| `@level-up` | `LevelUpEvent` | Level increased |
| `@level-down` | `LevelDownEvent` | Level decreased |
| `@error` | `ErrorEvent` | Error occurred |

#### Slots

| Slot | Description |
|------|-------------|
| `spin-button` | Custom spin button |

### `useSpinStudio`

Composable for SDK interaction.

```typescript
const {
  openWheel,      // (wheelId, options?) => Promise<WheelInstance>
  closeWheel,     // () => void
  preloadWheel,   // (wheelId) => Promise<void>
  isReady,        // Ref<boolean>
  isLoading,      // Ref<boolean>
} = useSpinStudio();
```

## Events

### PrizeWonEvent

```typescript
interface PrizeWonEvent {
  wheelId: string;
  sessionId: string;
  timestamp: number;
  prize: {
    id: string;
    name: string;
    type: 'discount_percentage' | 'discount_fixed' | 'free_shipping' |
          'free_product' | 'points' | 'coupon' | 'custom';
    value?: number;
    code?: string;
    imageUrl?: string;
  };
  redemptionCode?: string;  // e.g., "SPIN-ABC123"
}
```

## Storing Discounts (Recommended)

For production use, add discount fields to your Order model:

```prisma
model Order {
  // ... your existing fields
  discountCode     String?   // "SPIN-ABC123"
  discountType     String?   // "percentage", "fixed", "free_shipping"
  discountPercent  Float?    // 15
  discountAmount   Float?    // 15.00
  originalTotal    Float?    // Total before discount
}
```

> Without these fields, discounts work visually but won't be persisted in your database.

## TypeScript

Full TypeScript support included:

```typescript
import type {
  SpinWheelProps,
  SpinWheelRef,
  WheelData,
  Prize,
  PrizeWonEvent,
  SpinResult,
} from '@spin-studio/vue';
```

## Nuxt 3

For Nuxt 3, create a plugin:

```typescript
// plugins/spin-studio.client.ts
import { SpinStudioProvider } from '@spin-studio/vue';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component('SpinStudioProvider', SpinStudioProvider);
});
```

Then use in your app:

```vue
<!-- app.vue -->
<template>
  <SpinStudioProvider :config="{ apiKey: 'sk_live_xxx' }">
    <NuxtPage />
  </SpinStudioProvider>
</template>
```

## Related Packages

- **Vanilla JS:** `@spin-studio/sdk` - For any JavaScript project
- **React:** `@spin-studio/react` - For React applications

## License

MIT