# Hero Slider Implementation Design

**Date:** 2025-12-02
**Status:** Approved
**Architecture:** Next.js 14 Server Actions with Swiper.js

## Overview

Add hero slider functionality to AMSOIL dealer landing pages, enabling dealers to create and manage full-width carousel slides with images, text overlays, and call-to-action buttons through the existing slider editor interface.

## Requirements

### Functional Requirements

- Single hero slider per dealer (displayed at top of dealer index page)
- Save slider configuration to database as JSON
- Full-featured Swiper.js carousel with:
  - Auto-play (5 second delay)
  - Navigation arrows (previous/next)
  - Pagination dots
  - Keyboard navigation
- Full-width background images with text overlays
- Fixed slider behavior (no dealer-side configuration)

### Technical Constraints

- Next.js 14 App Router with Server Actions
- Prisma + PostgreSQL database
- Existing SliderEditorContext with SliderContent types
- Store as JSON field in Dealer model (denormalized)

## Architecture

### Data Flow

```
[Dashboard] -> Opens Editor Modal with initialSliderContent
     |
     v
[SliderEditorContext] -> User edits slides
     |
     v
[Save & Publish] -> Calls saveHeroSlider() Server Action
     |
     v
[Server Action] -> Auth check -> Update dealer.heroSlider JSON
     |
     v
[Dealer Page] -> SSR fetch heroSlider -> Render HeroSlider component -> Swiper.js
```

### Component Architecture

**Server Components:**

- Dealer page (fetches slider from DB)
- Dashboard (fetches initial slider for editor)

**Client Components:**

- SliderEditorModal (existing)
- SliderEditorContext (existing, updated saveAndPublish)
- HeroSlider (new - Swiper.js wrapper)

## Implementation Details

### 1. Database Schema

**Add to `prisma/schema.prisma`:**

```prisma
model Dealer {
  // ... existing fields ...
  heroSlider Json? // SliderContent: { id, slides: Slide[], updatedAt, publishedAt? }
}
```

**Migration:**

```bash
npx prisma migrate dev --name add_hero_slider
```

**JSON Structure:**

```typescript
{
  id: string; // UUID
  slides: [
    {
      id: string; // UUID
      order: number;
      image: { url: string; source: 'library' | 'upload' };
      headline: string;
      headlineFormatting?: { color?: string; bold?: boolean; italic?: boolean };
      subheading: string;
      subheadingFormatting?: { color?: string; bold?: boolean; italic?: boolean };
      button: { text: string; url: string };
    }
  ];
  updatedAt: Date;
  publishedAt?: Date;
}
```

### 2. Server Actions

**Create `/app/actions/slider.ts`:**

```typescript
'use server';

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { SliderContent } from '@/types/slider';

export async function saveHeroSlider(sliderContent: SliderContent) {
  // 1. Authenticate user
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) {
    throw new Error('Unauthorized');
  }

  // 2. Verify dealer exists
  const dealer = await prisma.dealer.findUnique({
    where: { userId: session.user.id },
  });
  if (!dealer) {
    throw new Error('Dealer not found');
  }

  // 3. Validate slider content
  if (!sliderContent.slides || sliderContent.slides.length === 0) {
    throw new Error('Slider must have at least one slide');
  }

  // 4. Update database
  await prisma.dealer.update({
    where: { id: dealer.id },
    data: {
      heroSlider: sliderContent as any, // Prisma JSON type
      updatedAt: new Date(),
    },
  });

  return { success: true };
}

export async function getHeroSlider(userId: string) {
  const dealer = await prisma.dealer.findUnique({
    where: { userId },
    select: { heroSlider: true },
  });

  return dealer?.heroSlider as SliderContent | null;
}
```

### 3. Slider Editor Integration

**Update `/app/components/SliderEditorContext.tsx`:**

```typescript
import { saveHeroSlider } from '@/app/actions/slider';

const saveAndPublish = useCallback(async () => {
  try {
    // 1. Mark as published
    const contentToSave = {
      ...sliderContent,
      publishedAt: new Date(),
    };

    // 2. Call Server Action
    await saveHeroSlider(contentToSave);

    // 3. Update local state
    setSliderContent(contentToSave);
    setFormState((prev) => ({
      ...prev,
      hasUnsavedChanges: false,
    }));
  } catch (error) {
    console.error('Failed to save slider:', error);
    // TODO: Show error toast notification
    throw error; // Re-throw for modal to handle
  }
}, [sliderContent]);
```

**Dashboard integration:**

```typescript
// In dashboard, when opening editor modal
const session = await getServerSession(authOptions);
const currentSlider = await getHeroSlider(session.user.id);

// Pass to modal:
<SliderEditorProvider initialSliderContent={currentSlider || undefined}>
  <SliderEditorModal />
</SliderEditorProvider>
```

### 4. Swiper.js Display Component

**Install dependency:**

```bash
npm install swiper
```

**Create `/components/HeroSlider.tsx`:**

```typescript
'use client'

import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination, Keyboard } from 'swiper/modules';
import { SliderContent } from '@/types/slider';

// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';

interface HeroSliderProps {
  sliderContent: SliderContent;
}

export function HeroSlider({ sliderContent }: HeroSliderProps) {
  return (
    <Swiper
      modules={[Autoplay, Navigation, Pagination, Keyboard]}
      autoplay={{
        delay: 5000,
        disableOnInteraction: false
      }}
      navigation
      pagination={{ clickable: true }}
      keyboard={{ enabled: true }}
      loop={sliderContent.slides.length > 1}
      className="hero-slider"
    >
      {sliderContent.slides.map((slide) => (
        <SwiperSlide key={slide.id}>
          <div
            className="slide-content"
            style={{
              backgroundImage: `url(${slide.image.url})`,
              backgroundSize: 'cover',
              backgroundPosition: 'center',
            }}
          >
            <div className="slide-overlay">
              {slide.headline && (
                <h1
                  style={{
                    color: slide.headlineFormatting?.color,
                    fontWeight: slide.headlineFormatting?.bold ? 'bold' : 'normal',
                    fontStyle: slide.headlineFormatting?.italic ? 'italic' : 'normal'
                  }}
                >
                  {slide.headline}
                </h1>
              )}

              {slide.subheading && (
                <p
                  style={{
                    color: slide.subheadingFormatting?.color,
                    fontWeight: slide.subheadingFormatting?.bold ? 'bold' : 'normal',
                    fontStyle: slide.subheadingFormatting?.italic ? 'italic' : 'normal'
                  }}
                >
                  {slide.subheading}
                </p>
              )}

              {slide.button.text && slide.button.url && (
                <a
                  href={slide.button.url}
                  className="slide-button"
                >
                  {slide.button.text}
                </a>
              )}
            </div>
          </div>
        </SwiperSlide>
      ))}
    </Swiper>
  );
}
```

**Render on dealer page:**

```typescript
// In dealer/[subdomain]/page.tsx or similar
import { HeroSlider } from '@/components/HeroSlider';

export default async function DealerPage({ params }) {
  const dealer = await prisma.dealer.findUnique({
    where: { subdomain: params.subdomain },
    select: { heroSlider: true, /* other fields */ }
  });

  return (
    <>
      {dealer?.heroSlider && (
        <HeroSlider sliderContent={dealer.heroSlider as SliderContent} />
      )}
      {/* Rest of dealer page */}
    </>
  );
}
```

### 5. Styling

**Add to `/app/globals.css`:**

```css
/* Hero Slider */
.hero-slider {
  width: 100%;
  height: 500px;
}

.hero-slider .swiper-slide {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

.hero-slider .slide-content {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.hero-slider .slide-overlay {
  position: relative;
  z-index: 10;
  text-align: center;
  color: white;
  padding: 2rem;
  max-width: 800px;
}

.hero-slider .slide-overlay::before {
  content: '';
  position: absolute;
  inset: -2rem;
  background: rgba(0, 0, 0, 0.3);
  z-index: -1;
  border-radius: 8px;
}

.hero-slider .slide-overlay h1 {
  font-size: 3rem;
  font-weight: bold;
  margin-bottom: 1rem;
}

.hero-slider .slide-overlay p {
  font-size: 1.5rem;
  margin-bottom: 2rem;
}

.hero-slider .slide-button {
  display: inline-block;
  padding: 1rem 2rem;
  background: #ffd700; /* AMSOIL accent */
  color: #003366; /* AMSOIL primary */
  text-decoration: none;
  border-radius: 4px;
  font-weight: bold;
  transition: background 0.3s;
}

.hero-slider .slide-button:hover {
  background: #ffc700;
}

/* Swiper navigation */
.hero-slider .swiper-button-next,
.hero-slider .swiper-button-prev {
  color: white;
}

.hero-slider .swiper-pagination-bullet {
  background: white;
  opacity: 0.5;
}

.hero-slider .swiper-pagination-bullet-active {
  background: #ffd700;
  opacity: 1;
}

/* Responsive */
@media (max-width: 768px) {
  .hero-slider {
    height: 300px;
  }

  .hero-slider .slide-overlay h1 {
    font-size: 2rem;
  }

  .hero-slider .slide-overlay p {
    font-size: 1.25rem;
  }
}
```

## Error Handling

### Edge Cases

| Scenario                                  | Handling                                                                                       |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------- |
| No slider configured (`heroSlider: null`) | Don't render HeroSlider component; dealer page shows other content only                        |
| Empty slides array                        | Server Action validates and rejects; editor prevents deleting last slide                       |
| Missing image URL                         | Editor should validate before allowing publish; display can skip slide or show fallback        |
| Database save failure                     | Server Action throws error; editor modal displays error toast and keeps modal open             |
| Unauthorized access                       | Server Action throws "Unauthorized"; editor displays login prompt                              |
| Concurrent edits                          | Not a concern (single user per dealer); future: add optimistic locking with `updatedAt` checks |

### User Experience

**Success flow:**

1. Dealer clicks "Edit Hero Slider" in dashboard
2. Modal opens with current slider (or empty default)
3. Dealer edits slides, uploads images, formats text
4. Clicks "Save & Publish"
5. Loading indicator appears
6. Success: Modal closes, toast notification "Slider published!"
7. Dealer page immediately shows updated slider

**Error flow:**

1. Save fails (network, auth, validation)
2. Error toast appears: "Failed to save slider. Please try again."
3. Modal stays open, no data lost
4. Dealer can retry save

## Testing Checklist

Since no testing framework is configured, manual testing required:

- [ ] Database migration runs cleanly
- [ ] Server Action authenticates correctly
- [ ] Server Action rejects unauthenticated requests
- [ ] Slider saves to database (check via Prisma Studio)
- [ ] Dashboard loads existing slider into editor
- [ ] New dealers get empty slider (one default slide)
- [ ] Dealer page renders slider with all slides
- [ ] Auto-play works (5 second intervals)
- [ ] Navigation arrows work
- [ ] Pagination dots work and are clickable
- [ ] Keyboard arrows control slides
- [ ] Text formatting (color, bold, italic) displays correctly
- [ ] Button links work
- [ ] Responsive design works on mobile (300px height)
- [ ] Responsive design works on desktop (500px height)
- [ ] No slider (null) doesn't break dealer page
- [ ] Saving shows loading state
- [ ] Errors display toast notifications
- [ ] Modal closes on successful save

## Migration Path

**Existing dealers:**

- Start with `heroSlider: null`
- Dealer page shows no slider section
- Dashboard shows "Create Hero Slider" button
- First save creates the slider

**Future enhancements:**

- Image optimization (convert uploads to WebP, generate responsive sizes)
- Slider analytics (track slide views, button clicks)
- A/B testing different slide variations
- Video backgrounds instead of images
- Animation effects (fade, zoom, parallax)
- Multiple sliders per dealer (testimonials, products, etc.)

## Files Modified/Created

**Modified:**

- `prisma/schema.prisma` - Add heroSlider field
- `app/components/SliderEditorContext.tsx` - Implement saveAndPublish
- `app/globals.css` - Add hero slider styles

**Created:**

- `app/actions/slider.ts` - Server Actions for save/get
- `components/HeroSlider.tsx` - Swiper.js display component

**Dependencies:**

- `swiper` (npm package)

## Deployment Steps

1. Install Swiper: `npm install swiper`
2. Run database migration: `npx prisma migrate dev --name add_hero_slider`
3. Create Server Actions file: `app/actions/slider.ts`
4. Create HeroSlider component: `components/HeroSlider.tsx`
5. Update SliderEditorContext: implement `saveAndPublish`
6. Add Swiper styles to: `app/globals.css`
7. Integrate into dealer page: conditionally render `<HeroSlider />`
8. Integrate into dashboard: load initial slider when opening editor
9. Test all scenarios from checklist
10. Deploy to production

## Questions for Future Consideration

- Should we add a preview mode to the editor (real Swiper.js preview instead of static carousel)?
- Should we allow dealers to reorder slides via drag-and-drop?
- Should we add slide-level analytics (views, click-through rates)?
- Should we support video backgrounds in addition to images?
- Should we add more transition effects (fade, flip, cube)?
