npm package: @newschools/sdk
Description: New Schools SDK - Multi-framework components and modules for integrating New Schools learning content
Latest version: 0.2.8

# @newschools/sdk

Nuxt module for integrating New Schools learning content into your applications with automatic organization theming.

## Features

✨ **Auto-imported composables** - `useAsyncNewSchools` and `useOrganization` available everywhere  
🎨 **Automatic theming** - Organization brand colors applied as CSS variables  
🔒 **Type-safe** - Full TypeScript support with entity types  
⚡ **SSR-compatible** - Built on Nuxt's `useAsyncData`  
🎯 **Ready-to-use components** - EntityHero, EntityBentoGrid, EntityCard  
📐 **Smart layouts** - EntityBentoGrid automatically generates responsive layouts  
🔑 **API key authentication** - Secure access to your organization's content

## Installation

```bash
# Using npm
npm install @newschools/sdk

# Using yarn
yarn add @newschools/sdk

# Using pnpm
pnpm add @newschools/sdk

# Using bun
bun add @newschools/sdk
```

## Setup

### 1. Add module to `nuxt.config.ts`

```typescript
export default defineNuxtConfig({
  modules: ["@newschools/sdk"],

  newschools: {
    apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
    organizationSlug: "my-organization", // Optional: auto-fetch org data and apply brand colors
  },
});
```

### 2. Configure API key

Add your New Schools API key to `.env`:

```env
NUXT_PUBLIC_NEWSCHOOLS_API_KEY=ns_live_xxxxxxxxxxxxx
```

### 3. (Optional) Configure organization

If you want to automatically load organization data and apply brand theming:

```typescript
export default defineNuxtConfig({
  modules: ["@newschools/sdk"],

  newschools: {
    apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
    organizationSlug: "webdesignwill", // Loads org data on init
  },
});
```

When `organizationSlug` is set, the SDK will:

- ✅ Auto-fetch organization data on app initialization
- ✅ Apply brand colors (`primary`, `secondary`, `tertiary`) as CSS variables
- ✅ Make organization data available via `useOrganization()` composable

## Automatic Brand Theming

The SDK automatically applies your organization's brand colors as CSS variables:

```typescript
// In nuxt.config.ts
export default defineNuxtConfig({
  newschools: {
    organizationSlug: "my-org", // Has primary: "#FF0000"
  },
});
```

The SDK sets:

- `--ns-color-primary` → `#FF0000`
- `--ns-color-secondary` → (your secondary color)
- `--ns-color-tertiary` → (your tertiary color)

If colors are not set or cleared by admin, they revert to SDK defaults:

- `--ns-color-primary` → `#4a3f8f` (Deep Purple)
- `--ns-color-secondary` → `#ff6b6b` (Vibrant Coral)
- `--ns-color-tertiary` → `#0fa3b1` (Rich Teal)

Use these variables in your components:

```vue
<template>
  <div class="my-component">
    <h1>Automatically themed!</h1>
  </div>
</template>

<style scoped>
.my-component {
  background: var(--ns-color-primary);
  color: white;
}

h1 {
  border-bottom: 2px solid var(--ns-color-secondary);
}
</style>
```

## Usage

### Organization Data

Access organization data loaded by the SDK:

```vue
<script setup lang="ts">
const organization = useOrganization();
</script>

<template>
  <div v-if="organization">
    <h1>{{ organization.name }}</h1>
    <img v-if="organization.coverImageUrl" :src="organization.coverImageUrl" />
  </div>
</template>
```

### Pre-built Components

#### EntityHero

Display entity header with cover image and branding. Supports 4 layout variants:

**Basic Usage:**

```vue
<template>
  <EntityHero :entity="organization" variant="left">
    <template #sub-title>
      <p>Welcome to our learning platform</p>
    </template>

    <template #content>
      <div>Custom content area for CTAs, forms, etc.</div>
    </template>
  </EntityHero>
</template>

<script setup lang="ts">
const organization = useOrganization();
</script>
```

**Layout Variants:**

- `variant="left"` (default) - Image on left, content on right (grid on tablet+, stacked on mobile)
- `variant="right"` - Content on left, image on right (grid on tablet+, stacked on mobile)
- `variant="fullscreen"` - Full screen image with content overlay at bottom left (all screen sizes)
- `variant="horizontal"` - Image on top (50vh with clamp), content below (stacked on all sizes)

**Variant Examples:**

```vue
<!-- Fullscreen: Content overlays image -->
<EntityHero :entity="organization" variant="fullscreen">
  <template #sub-title>Journey subtitle</template>
  <template #content>
    <p>This content appears over the image, under the title.</p>
  </template>
</EntityHero>

<!-- Horizontal: Image top, content bottom -->
<EntityHero :entity="organization" variant="horizontal">
  <template #sub-title>Course overview</template>
  <template #content>
    <div>Course details and enrollment form</div>
  </template>
</EntityHero>

<!-- Right: Reversed grid layout -->
<EntityHero :entity="organization" variant="right">
  <template #sub-title>Featured content</template>
  <template #content>
    <button>Get Started</button>
  </template>
</EntityHero>
```

**Responsive Behavior:**

- Title and subtitle always overlay the image (all variants)
- `left` and `right` variants stack vertically on mobile (<768px)
- `fullscreen` maintains overlay layout on all screen sizes
- `horizontal` uses flexible image height: `clamp(40vh, 50vh, 60vh)`

#### EntityBentoGrid (Recommended)

Display entities in a responsive bento grid with automatic layout:

```vue
<template>
  <!-- Simple: Auto-renders journeys as cards -->
  <EntityBentoGrid :items="journeys" />

  <!-- Advanced: Custom card rendering -->
  <EntityBentoGrid :items="journeys">
    <template #item="{ item, index }">
      <EntityCard :image-url="item.coverImageUrl" :image-alt="item.title">
        <template #title>
          <h3>{{ item.title }}</h3>
        </template>
        <template #description>
          <p>{{ item.description }}</p>
        </template>
      </EntityCard>
    </template>
  </EntityBentoGrid>
</template>

<script setup lang="ts">
const { data: journeys } = await useAsyncNewSchools("journeys");
</script>
```

**What it does:**

- ✅ Automatically generates optimized layouts for 1-6+ items
- ✅ Works with any entity type (journeys, teachers, waypoints, activities)
- ✅ Default rendering extracts `title`, `description`, and images automatically
- ✅ Fully customizable via `#item` slot
- ✅ Responsive (mobile-first, 3-column grid on tablet+)

#### BentoGrid (Advanced)

For complete manual control, use BentoGrid directly:

Create responsive masonry-style layouts:

```vue
<template>
  <BentoGrid>
    <!-- 2x2 Featured card -->
    <BentoGridItem :col-span="2" :row-span="2">
      <EntityCard image-url="/hero.jpg" image-alt="Featured content">
        <template #title>
          <h3>Featured Content</h3>
        </template>
        <template #description>
          <p>Main featured content with prominent display</p>
        </template>
      </EntityCard>
    </BentoGridItem>

    <!-- Standard cards -->
    <BentoGridItem>
      <EntityCard image-url="/card.jpg">
        <template #title>
          <h3>Regular Card</h3>
        </template>
        <template #description>
          <p>Standard card content</p>
        </template>
      </EntityCard>
    </BentoGridItem>

    <!-- Explicit positioning (advanced) -->
    <BentoGridItem :col-span="2" :row-span="2" :col-start="2" :row-start="3">
      <EntityCard image-url="/bottom-right.jpg">
        <template #title>
          <h3>Bottom Right</h3>
        </template>
        <template #description>
          <p>Explicitly positioned at column 2, row 3</p>
        </template>
      </EntityCard>
    </BentoGridItem>
  </BentoGrid>
</template>
```

### Fetching Learning Content

The `useAsyncNewSchools` composable is auto-imported and available everywhere.

#### Display journeys in a bento grid

```vue
<script setup lang="ts">
const { data: journeys } = await useAsyncNewSchools("journeys");
</script>

<template>
  <EntityBentoGrid v-if="journeys" :items="journeys" />
</template>
```

#### Fetch all journeys

```vue
<script setup lang="ts">
const { data: journeys, error } = await useAsyncNewSchools("journeys");
</script>

<template>
  <div v-if="journeys">
    <div v-for="journey in journeys" :key="journey.id">
      <h2>{{ journey.title }}</h2>
      <p>{{ journey.description }}</p>
    </div>
  </div>
</template>
```

#### Fetch a specific journey

```vue
<script setup lang="ts">
const route = useRoute();
const { data: journey } = await useAsyncNewSchools("journey", {
  id: route.params.slug,
});
</script>

<template>
  <div v-if="journey">
    <h1>{{ journey.title }}</h1>
    <p>{{ journey.description }}</p>
  </div>
</template>
```

#### Fetch waypoints for a journey

```vue
<script setup lang="ts">
const { data: waypoints } = await useAsyncNewSchools("waypoints", {
  journeyId: "my-journey-slug",
});
</script>
```

#### Fetch a specific waypoint

```vue
<script setup lang="ts">
const { data: waypoint } = await useAsyncNewSchools("waypoint", {
  journeyId: "journey-slug",
  waypointId: "waypoint-slug",
});
</script>
```

## TypeScript Support

Full type safety for all entities:

```typescript
import type {
  Organisation,
  OrganisationBrand,
  Journey,
  Waypoint,
  Activity,
  JourneyTheme,
  WaypointType,
  LayoutMode,
} from "@newschools/sdk";

// Types are automatically inferred in composables
const organization = useOrganization();
// organization is typed as Ref<Organisation | null>

const { data: journeys } = await useAsyncNewSchools("journeys");
// journeys is typed as Journey[] | null
```

## Module Configuration

All configuration options for `nuxt.config.ts`:

```typescript
export default defineNuxtConfig({
  newschools: {
    /**
     * New Schools API key (required)
     * Can also be set via NUXT_PUBLIC_NEWSCHOOLS_API_KEY env variable
     */
    apiKey: string;

    /**
     * Base URL for New Schools API
     * Default: "https://newschools.ai"
     */
    baseUrl?: string;

    /**
     * Organization slug for auto-fetch and theming
     * When set, SDK will:
     * - Automatically fetch organization data on init
     * - Apply brand colors as CSS variables
     * - Make data available via useOrganization()
     *
     * Example: "my-organization"
     */
    organizationSlug?: string;
  },
});
```

### Do you need `baseUrl`?

**No**, unless you're using a custom API endpoint or testing against a development server.

The default value is `"https://newschools.ai"`, which points to production.

```typescript
// ✅ Minimal config (most common)
export default defineNuxtConfig({
  newschools: {
    apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
  },
});

// ✅ With organization theming
export default defineNuxtConfig({
  newschools: {
    apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
    organizationSlug: "webdesignwill",
  },
});

// ⚠️ Only needed for custom environments
export default defineNuxtConfig({
  newschools: {
    apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
    baseUrl: "https://staging.newschools.ai", // Testing against staging
  },
});
```

## Available CSS Variables

The SDK provides themeable CSS tokens:

### Brand Colors (Automatically set from organization)

- `--ns-color-primary` - Primary brand color
- `--ns-color-secondary` - Secondary brand color
- `--ns-color-tertiary` - Tertiary brand color

### Additional Tokens

- `--ns-color-grey-800`, `--ns-color-grey-900`
- `--ns-spacing-xs` through `--ns-spacing-3xl`
- `--ns-border-radius-sm` through `--ns-border-radius-2xl`
- `--ns-shadow-sm` through `--ns-shadow-xl`

See [tokens.css](./nuxt/src/runtime/styles/tokens.css) for the complete list.

## Advanced Usage

### Manual Client (Without Nuxt Composables)

For use in server routes, plugins, or non-component contexts:

```typescript
import { createClient } from "@newschools/sdk";

const client = createClient({
  apiKey: "ns_live_xxxxxxxxxxxxx",
  baseUrl: "https://newschools.ai", // optional
});

const journeys = await client.get<Journey[]>("/journeys");
const journey = await client.get<Journey>("/journeys/my-slug");
```

## Environment Variables

| Variable                          | Description              | Required | Default               |
| --------------------------------- | ------------------------ | -------- | --------------------- |
| `NUXT_PUBLIC_NEWSCHOOLS_API_KEY`  | Your New Schools API key | Yes      | -                     |
| `NUXT_PUBLIC_NEWSCHOOLS_BASE_URL` | API base URL             | No       | https://newschools.ai |

## Components Reference

### `<EntityHero>`

Display entity header with cover image and branding

**Props:**

- `entity: Organisation` - Entity object (Organization, Journey, etc.) (required)
- `variant?: 'left' | 'right' | 'fullscreen' | 'horizontal'` - Layout variant (default: 'left')

**Slots:**

- `sub-title` - Content below entity name (overlays image)
- `content` - Main content area (position varies by variant)

**Layout Behavior:**

- `left`: Grid layout (image left, content right) on tablet+, stacks on mobile
- `right`: Grid layout (content left, image right) on tablet+, stacks on mobile
- `fullscreen`: Content overlays image at bottom left (all screen sizes)
- `horizontal`: Image top with flexible height, content below (all screen sizes)

### `<EntityBentoGrid>`

Automatically display entities in a responsive bento grid layout

**Props:**

- `items: any[]` - Array of entities (journeys, teachers, waypoints, etc.) (required)
- `key-field?: string` - Custom key field for v-for (default: 'id')

**Slots:**

- `item` - Scoped slot for custom item rendering
  - `item` - The entity object
  - `index` - Item index

**Auto-extracts fields:**

- Title: `title` or `name`
- Description: `description` or `summary`
- Image: `coverImageUrl`, `avatarUrl`, or `imageUrl`

**Layout patterns:**

- 1 item: Full width
- 2 items: 2/3 + 1/3 split
- 3 items: 1 large (2x2) + 2 small stacked
- 4 items: 1 large (2x2) + 2 small + 1 wide bottom
- 5 items: 1 large + 4 small corners
- 6+ items: Mirrored pattern (repeating)

### `<BentoGrid>`

Responsive masonry-style grid layout (advanced manual control)

### `<BentoGridItem>`

Grid item with flexible sizing and optional explicit positioning

**Props:**

- `col-span?: number` - Column span (1-3, default: 1)
- `row-span?: number` - Row span (1-2, default: 1)
- `col-start?: number` - Column start position (1-3, optional for explicit positioning)
- `row-start?: number` - Row start position (1+, optional for explicit positioning)

**Note:** When `col-start` and `row-start` are omitted, items use CSS Grid auto-placement. Use explicit positioning for complex layouts where auto-flow doesn't achieve the desired result.

### `<EntityCard>`

Card component for displaying content with automatic text clamping

**Props:**

- `image-url?: string` - Cover image URL
- `image-alt?: string` - Image alt text

**Slots:**

- `title` - Card title (automatically clamped to 2 lines with ellipsis)
- `description` - Card description (automatically clamped to 3 lines with ellipsis)

**Example:**

```vue
<EntityCard
  image-url="https://example.com/image.jpg"
  image-alt="Course thumbnail"
>
  <template #title>
    <h3>{{ journey.title }}</h3>
  </template>
  <template #description>
    <p>{{ journey.description }}</p>
  </template>
</EntityCard>
```

### `<EntityImage>`

Optimized image component with loading states

**Props:**

- `src?: string` - Image URL
- `alt: string` - Alt text

## Composables Reference

### `useOrganization()`

Access globally loaded organization data

**Returns:** `Ref<Organisation | null>`

### `useAsyncNewSchools(resource, params?)`

Fetch data from New Schools v1 API with SSR support

**Resources:**

- `'journeys'` - List all journeys
- `'journey'` - Get single journey
- `'waypoints'` - List waypoints
- `'waypoint'` - Get single waypoint
- `'activities'` - List activities
- `'activity'` - Get single activity

**Returns:** `AsyncData<T>`

## License

MIT