mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Update mobile app UI with premium design system
- Add design guidelines to CLAUDE.md - Redesign navigation with boutique styling - Add video support to chat with compression - Update matches/dares/quiz screens with gold/rose accents - Improve packs browsing with category tabs - Add SwipeCardPremium component - Update theme with premium color palette - Add video cleanup edge function and migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
feb7657562
commit
cac7a2a31d
31 files changed
+6123
-1312
No files matched your search
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Important: Design Guidelines
|
||||||
|
|
||||||
|
**Always read `apps/mobile/DESIGN.md` before making UI/UX changes to the mobile app.** This file contains the design system, visual language, and styling patterns that must be followed for consistency. Agents and subagents should read this file when:
|
||||||
|
- Creating new screens or components
|
||||||
|
- Modifying existing UI elements
|
||||||
|
- Adding animations or visual effects
|
||||||
|
- Working with colors, typography, or spacing
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Sauci is a couples intimacy app featuring swipeable question packs with partner matching. Users swipe on questions (yes/no/maybe), and when both partners answer positively, a "match" is created that unlocks a chat thread for that question.
|
Sauci is a couples intimacy app featuring swipeable question packs with partner matching. Users swipe on questions (yes/no/maybe), and when both partners answer positively, a "match" is created that unlocks a chat thread for that question.
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
# Sauci Design System
|
||||||
|
|
||||||
|
A boutique, premium design framework for the Sauci couples intimacy app.
|
||||||
|
|
||||||
|
## Brand Identity
|
||||||
|
|
||||||
|
**Aesthetic:** Boutique lingerie store meets modern dating app. Think Ann Summers sophistication - sensual but tasteful, premium but accessible, intimate but playful.
|
||||||
|
|
||||||
|
**Mood:** Romantic, luxurious, warm, inviting, confident
|
||||||
|
|
||||||
|
**Key Principles:**
|
||||||
|
|
||||||
|
- Glass-morphism for depth and elegance
|
||||||
|
- Dark backgrounds with warm accents
|
||||||
|
- Primary gradient (rose → purple) as the core brand signature
|
||||||
|
- Gold reserved for premium/dares features only
|
||||||
|
- Gradients for visual interest and brand recognition
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Color Hierarchy
|
||||||
|
|
||||||
|
### 1. Brand Colors (Primary Palette)
|
||||||
|
|
||||||
|
The core brand identity. Used for primary actions, key UI elements, and brand recognition.
|
||||||
|
|
||||||
|
| Name | Hex | RGB | Usage |
|
||||||
|
|------|-----|-----|-------|
|
||||||
|
| **Primary** | `#e94560` | 233, 69, 96 | Primary buttons, active states, key accents |
|
||||||
|
| **Primary Dark** | `#c73a52` | 199, 58, 82 | Pressed states, darker variants |
|
||||||
|
| **Secondary** | `#9b59b6` | 155, 89, 182 | Secondary accents, gradient endpoints |
|
||||||
|
| **Secondary Dark** | `#8e44ad` | 142, 68, 173 | Pressed states, darker variants |
|
||||||
|
|
||||||
|
**Primary Gradient:** `#e94560 → #9b59b6` (Rose to Purple)
|
||||||
|
- Direction: Left to right (horizontal) or top to bottom (vertical)
|
||||||
|
- Use for: Primary buttons, match celebrations, brand moments
|
||||||
|
|
||||||
|
### 2. Premium/Luxury Colors
|
||||||
|
|
||||||
|
The boutique palette. Used for premium features, upgrades, and luxury moments.
|
||||||
|
|
||||||
|
| Name | Hex | RGB | Usage |
|
||||||
|
|------|-----|-----|-------|
|
||||||
|
| **Gold** | `#D4AF37` | 212, 175, 55 | Premium badges, Dares feature, luxury accents |
|
||||||
|
| **Gold Dark** | `#B8860B` | 184, 134, 11 | Gold gradient endpoint |
|
||||||
|
| **Rose** | `#E8A4AE` | 232, 164, 174 | Quiz feature, soft premium accents |
|
||||||
|
| **Rose Dark** | `#D4919B` | 212, 145, 155 | Rose gradient endpoint |
|
||||||
|
| **Champagne** | `#F7E7CE` | 247, 231, 206 | Highlights, premium text accents |
|
||||||
|
|
||||||
|
### 3. Feature Colors
|
||||||
|
|
||||||
|
Each core feature has a designated accent color for consistency and recognition.
|
||||||
|
|
||||||
|
| Feature | Primary Color | Hex | Gradient |
|
||||||
|
|---------|---------------|-----|----------|
|
||||||
|
| **Swipe/Match** | Primary | `#e94560` | Primary gradient (rose → purple) |
|
||||||
|
| **Quiz** | Rose | `#E8A4AE` | `#E8A4AE → #D4919B` |
|
||||||
|
| **Dares** | Gold | `#D4AF37` | `#D4AF37 → #B8860B` |
|
||||||
|
| **Chat** | Primary | `#e94560` | Primary gradient |
|
||||||
|
| **Profile** | Secondary | `#9b59b6` | Secondary gradient |
|
||||||
|
|
||||||
|
**Usage Rules:**
|
||||||
|
- Feature icons use their designated color
|
||||||
|
- Feature headers/titles can use the color as an accent
|
||||||
|
- Feature-specific buttons should use the feature color
|
||||||
|
- Keep the feature color consistent across all screens for that feature
|
||||||
|
|
||||||
|
### 4. Background Colors
|
||||||
|
|
||||||
|
Dark, rich backgrounds that make content pop.
|
||||||
|
|
||||||
|
| Name | Hex | RGBA | Usage |
|
||||||
|
|------|-----|------|-------|
|
||||||
|
| **Background** | `#0d0d1a` | 13, 13, 26 | Primary app background |
|
||||||
|
| **Background Light** | `#1a1a2e` | 26, 26, 46 | Cards, elevated surfaces |
|
||||||
|
| **Surface** | — | `rgba(22, 33, 62, 0.6)` | Glass overlays |
|
||||||
|
| **Surface Solid** | `#16213e` | 22, 33, 62 | Non-transparent surfaces |
|
||||||
|
|
||||||
|
### 5. Glass/Surface Colors
|
||||||
|
|
||||||
|
For the glass-morphism effect that defines the UI.
|
||||||
|
|
||||||
|
| Name | RGBA | Usage |
|
||||||
|
|------|------|-------|
|
||||||
|
| **Glass Background** | `rgba(22, 33, 62, 0.4)` | Standard glass cards |
|
||||||
|
| **Glass Background Heavy** | `rgba(22, 33, 62, 0.6)` | Emphasized glass cards |
|
||||||
|
| **Glass Border** | `rgba(255, 255, 255, 0.08)` | Subtle borders |
|
||||||
|
| **Glass Border Light** | `rgba(255, 255, 255, 0.12)` | Prominent borders |
|
||||||
|
| **Glass Highlight** | `rgba(255, 255, 255, 0.05)` | Top edge shine |
|
||||||
|
|
||||||
|
### 6. Text Colors
|
||||||
|
|
||||||
|
| Name | RGBA | Usage |
|
||||||
|
|------|------|-------|
|
||||||
|
| **Text** | `#ffffff` | Primary text, headings |
|
||||||
|
| **Text Secondary** | `rgba(255, 255, 255, 0.7)` | Secondary text, descriptions |
|
||||||
|
| **Text Tertiary** | `rgba(255, 255, 255, 0.5)` | Disabled, hints, captions |
|
||||||
|
|
||||||
|
### 7. Semantic Colors
|
||||||
|
|
||||||
|
For system feedback and states.
|
||||||
|
|
||||||
|
| Name | Hex | Usage |
|
||||||
|
|------|-----|-------|
|
||||||
|
| **Success** | `#2ECC71` | Yes responses, confirmations, positive states |
|
||||||
|
| **Warning** | `#F39C12` | Maybe responses, cautions |
|
||||||
|
| **Error** | `#E74C3C` | No responses, errors, destructive actions |
|
||||||
|
| **Muted** | `#6c757d` | Skip, disabled, inactive |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Swipe Response Colors
|
||||||
|
|
||||||
|
Consistent across all swipe interactions:
|
||||||
|
|
||||||
|
| Response | Color | Hex/RGBA | Icon |
|
||||||
|
|----------|-------|----------|------|
|
||||||
|
| **Yes** | Success | `#2ECC71` / `rgba(46, 204, 113, 0.4)` | Checkmark |
|
||||||
|
| **No** | Error | `#E74C3C` / `rgba(231, 76, 60, 0.4)` | X |
|
||||||
|
| **Maybe** | Warning | `#F39C12` / `rgba(243, 156, 18, 0.4)` | Question mark |
|
||||||
|
| **Skip** | Muted | `#6c757d` / `rgba(108, 117, 125, 0.4)` | Forward arrow |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gradients
|
||||||
|
|
||||||
|
### Brand Gradients
|
||||||
|
|
||||||
|
```
|
||||||
|
Primary: #e94560 → #9b59b6 (Rose to Purple)
|
||||||
|
Primary Subtle: rgba(233, 69, 96, 0.8) → rgba(155, 89, 182, 0.8)
|
||||||
|
Primary Reverse: #9b59b6 → #e94560 (Purple to Rose)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Premium Gradients
|
||||||
|
|
||||||
|
```
|
||||||
|
Premium Gold: #D4AF37 → #B8860B
|
||||||
|
Premium Rose: #E8A4AE → #D4919B
|
||||||
|
```
|
||||||
|
|
||||||
|
### Background Gradients
|
||||||
|
|
||||||
|
```
|
||||||
|
Background: #1a1a2e → #0d0d1a (Light to Dark)
|
||||||
|
Background Reverse: #0d0d1a → #1a1a2e (Dark to Light)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Boutique Card Gradients
|
||||||
|
|
||||||
|
For pack cards and premium surfaces:
|
||||||
|
|
||||||
|
```
|
||||||
|
Boutique Rose: rgba(233, 69, 96, 0.4) → rgba(155, 89, 182, 0.4)
|
||||||
|
Boutique Purple: rgba(155, 89, 182, 0.4) → rgba(233, 69, 96, 0.4)
|
||||||
|
Boutique Dusty: rgba(183, 110, 121, 0.35) → rgba(139, 69, 87, 0.35)
|
||||||
|
Boutique Gold: rgba(212, 175, 55, 0.25) → rgba(184, 134, 11, 0.25)
|
||||||
|
Boutique Midnight: rgba(22, 33, 62, 0.8) → rgba(13, 13, 26, 0.8)
|
||||||
|
Boutique Amethyst: rgba(142, 68, 173, 0.4) → rgba(44, 62, 80, 0.4)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component Patterns
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
| Variant | Background | Text | Border |
|
||||||
|
|---------|------------|------|--------|
|
||||||
|
| **Primary** | Primary gradient | White | None |
|
||||||
|
| **Secondary** | Glass background | White | Glass border |
|
||||||
|
| **Ghost** | Transparent | Primary | None |
|
||||||
|
| **Danger** | Error gradient | White | None |
|
||||||
|
| **Feature** | Feature color gradient | White | None |
|
||||||
|
|
||||||
|
### Cards
|
||||||
|
|
||||||
|
| Variant | Background | Border | Use Case |
|
||||||
|
|---------|------------|--------|----------|
|
||||||
|
| **Glass** | Glass background | Glass border | Standard content |
|
||||||
|
| **Glass Elevated** | Glass background heavy | Glass border light | Emphasized content |
|
||||||
|
| **Solid** | Background light | Glass border | Non-blur contexts |
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
|
||||||
|
| State | Border Color | Background |
|
||||||
|
|-------|--------------|------------|
|
||||||
|
| **Default** | Glass border | Glass background |
|
||||||
|
| **Focused** | Primary | Glass background |
|
||||||
|
| **Error** | Error | Glass background |
|
||||||
|
| **Disabled** | Glass border | Glass background (opacity 0.5) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tab Bar
|
||||||
|
|
||||||
|
| Element | Color |
|
||||||
|
|---------|-------|
|
||||||
|
| **Background** | `rgba(13, 13, 26, 0.92)` with blur |
|
||||||
|
| **Top Border** | `rgba(233, 69, 96, 0.2)` (primary tint) |
|
||||||
|
| **Active Icon** | Primary `#e94560` |
|
||||||
|
| **Inactive Icon** | Text secondary |
|
||||||
|
| **Badge** | Rose `#E8A4AE` |
|
||||||
|
|
||||||
|
## Play Button (Center Tab)
|
||||||
|
|
||||||
|
The main "play" button uses the primary brand gradient for maximum brand presence.
|
||||||
|
|
||||||
|
| Element | Color |
|
||||||
|
|---------|-------|
|
||||||
|
| **Glow** | Primary glow `rgba(233, 69, 96, 0.3)` |
|
||||||
|
| **Border** | `rgba(233, 69, 96, 0.4)` |
|
||||||
|
| **Highlight** | Primary → Secondary gradient |
|
||||||
|
| **Icon** | Primary `#e94560` |
|
||||||
|
|
||||||
|
## Radial Menu
|
||||||
|
|
||||||
|
Feature-specific colors for quick action items:
|
||||||
|
|
||||||
|
| Item | Variant | Color |
|
||||||
|
|------|---------|-------|
|
||||||
|
| **Match** | Primary | `#e94560` (core feature) |
|
||||||
|
| **Dares** | Gold | `#D4AF37` (premium/adventure) |
|
||||||
|
| **Quiz** | Rose | `#E8A4AE` (intimacy) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Icons (Locked)
|
||||||
|
|
||||||
|
**IMPORTANT:** These icons are locked and must be used consistently across the entire app (radial menu, page headers, buttons, etc.).
|
||||||
|
|
||||||
|
| Feature | Icon Name | Ionicons | Description |
|
||||||
|
|---------|-----------|----------|-------------|
|
||||||
|
| **Swipe/Match** | `flame` | 🔥 | Fire/passion - the core swiping experience |
|
||||||
|
| **Dares** | `flash` | ⚡ | Lightning bolt - adventure, excitement, energy |
|
||||||
|
| **Quiz** | `help-circle` | ❓ | Question mark - discovery, questions about each other |
|
||||||
|
| **Matches** (tab) | `heart` | ❤️ | Love heart - connections made between partners |
|
||||||
|
| **Profile** | `person` | 👤 | Person - user settings and profile |
|
||||||
|
| **Home** | `home` | 🏠 | Home - main dashboard |
|
||||||
|
| **Packs** | `layers` | 📚 | Layers - question pack collections |
|
||||||
|
|
||||||
|
**Usage Rules:**
|
||||||
|
- Always use the exact icon name specified above
|
||||||
|
- Use the feature's designated color with its icon
|
||||||
|
- Never swap icons between features
|
||||||
|
- Radial menu, page headers, and any feature references must use the same icon
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spacing Scale
|
||||||
|
|
||||||
|
| Token | Value | Usage |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| `xs` | 4px | Tight spacing, icon gaps |
|
||||||
|
| `sm` | 8px | Small gaps, compact layouts |
|
||||||
|
| `md` | 16px | Standard spacing |
|
||||||
|
| `lg` | 24px | Section spacing |
|
||||||
|
| `xl` | 32px | Large gaps |
|
||||||
|
| `xxl` | 48px | Major sections |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Border Radius Scale
|
||||||
|
|
||||||
|
| Token | Value | Usage |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| `xs` | 4px | Small elements, tags |
|
||||||
|
| `sm` | 8px | Buttons, small cards |
|
||||||
|
| `md` | 12px | Standard cards |
|
||||||
|
| `lg` | 16px | Large cards |
|
||||||
|
| `xl` | 24px | Modal corners |
|
||||||
|
| `xxl` | 32px | Large containers |
|
||||||
|
| `full` | 9999px | Pills, circles |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
| Style | Size | Weight | Line Height | Usage |
|
||||||
|
|-------|------|--------|-------------|-------|
|
||||||
|
| `largeTitle` | 34px | Bold | 41px | Screen titles |
|
||||||
|
| `title1` | 28px | Bold | 34px | Section headers |
|
||||||
|
| `title2` | 22px | Bold | 28px | Card titles |
|
||||||
|
| `title3` | 20px | SemiBold | 25px | Subsection headers |
|
||||||
|
| `headline` | 17px | SemiBold | 22px | Emphasized body |
|
||||||
|
| `body` | 17px | Regular | 22px | Body text |
|
||||||
|
| `callout` | 16px | Regular | 21px | Secondary body |
|
||||||
|
| `subhead` | 15px | Regular | 20px | Supporting text |
|
||||||
|
| `footnote` | 13px | Regular | 18px | Captions |
|
||||||
|
| `caption1` | 12px | Regular | 16px | Small labels |
|
||||||
|
| `caption2` | 11px | Regular | 13px | Tiny text |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shadow Scale
|
||||||
|
|
||||||
|
| Style | Values | Usage |
|
||||||
|
|-------|--------|-------|
|
||||||
|
| `none` | — | Flat elements |
|
||||||
|
| `sm` | 0 2px 4px rgba(0,0,0,0.1) | Subtle lift |
|
||||||
|
| `md` | 0 4px 8px rgba(0,0,0,0.15) | Standard elevation |
|
||||||
|
| `lg` | 0 8px 16px rgba(0,0,0,0.2) | Prominent elevation |
|
||||||
|
| `xl` | 0 12px 24px rgba(0,0,0,0.25) | Maximum elevation |
|
||||||
|
| `glow` | 0 0 20px {color}Glow | Accent glow effect |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Platform Considerations
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
- Use BlurView for glass effects
|
||||||
|
- Haptic feedback on interactions
|
||||||
|
- Native-feeling animations
|
||||||
|
|
||||||
|
### Android
|
||||||
|
- Fallback to solid semi-transparent backgrounds
|
||||||
|
- Reduced blur intensity for performance
|
||||||
|
- Adjusted shadow rendering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Do's and Don'ts
|
||||||
|
|
||||||
|
### Do
|
||||||
|
- Use the primary gradient for main CTAs
|
||||||
|
- Apply feature colors consistently within a feature
|
||||||
|
- Use glass effects for depth and hierarchy
|
||||||
|
- Keep text high-contrast (white on dark)
|
||||||
|
- Use gold/rose for premium moments
|
||||||
|
|
||||||
|
### Don't
|
||||||
|
- Mix feature colors within a single screen
|
||||||
|
- Use more than 2-3 accent colors per screen
|
||||||
|
- Apply the primary gradient to everything
|
||||||
|
- Use pure black (#000000) for backgrounds
|
||||||
|
- Forget to include the glow effect on premium elements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Reference
|
||||||
|
|
||||||
|
**Theme Source:** `apps/mobile/src/theme/index.ts`
|
||||||
|
|
||||||
|
All colors, gradients, spacing, and typography are defined in the theme file. Always import from theme rather than hardcoding values.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { theme } from '@/theme';
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const styles = {
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
color: theme.colors.premium.gold,
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -44,13 +44,28 @@ interface RadialMenuItem {
|
|||||||
icon: keyof typeof Ionicons.glyphMap;
|
icon: keyof typeof Ionicons.glyphMap;
|
||||||
label: string;
|
label: string;
|
||||||
angle: number; // degrees from top (0 = top, -45 = left, 45 = right)
|
angle: number; // degrees from top (0 = top, -45 = left, 45 = right)
|
||||||
color?: string;
|
variant: 'primary' | 'gold' | 'rose' | 'muted';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Brand color palette for navigation
|
||||||
|
const NAV_COLORS = {
|
||||||
|
// Primary brand colors (from logo)
|
||||||
|
primary: colors.primary,
|
||||||
|
primaryRgba: 'rgba(233, 69, 96, ',
|
||||||
|
secondary: colors.secondary,
|
||||||
|
secondaryRgba: 'rgba(155, 89, 182, ',
|
||||||
|
// Feature-specific colors
|
||||||
|
gold: colors.premium.gold,
|
||||||
|
goldRgba: 'rgba(212, 175, 55, ',
|
||||||
|
rose: colors.premium.rose,
|
||||||
|
roseRgba: 'rgba(232, 164, 174, ',
|
||||||
|
dark: '#0d0d1a',
|
||||||
|
};
|
||||||
|
|
||||||
const RADIAL_MENU_ITEMS: RadialMenuItem[] = [
|
const RADIAL_MENU_ITEMS: RadialMenuItem[] = [
|
||||||
{ id: 'dares', icon: 'flash', label: 'Dares', angle: -50, color: '#FF6B35' },
|
{ id: 'dares', icon: 'flash', label: 'Dares', angle: -50, variant: 'gold' },
|
||||||
{ id: 'match', icon: 'flame', label: 'Match', angle: 0 },
|
{ id: 'match', icon: 'flame', label: 'Match', angle: 0, variant: 'primary' },
|
||||||
{ id: 'quiz', icon: 'help-circle', label: 'Quiz', angle: 50, color: '#7B68EE' },
|
{ id: 'quiz', icon: 'help-circle', label: 'Quiz', angle: 50, variant: 'rose' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const RADIAL_DISTANCE = 100; // Distance from center button
|
const RADIAL_DISTANCE = 100; // Distance from center button
|
||||||
@@ -73,24 +88,26 @@ function PlayTabButton({ accessibilityState }: PlayTabButtonProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.playButtonContainer}>
|
<View style={styles.playButtonContainer}>
|
||||||
{/* Outer glow effect */}
|
{/* Outer glow effect - primary brand */}
|
||||||
<View style={[styles.playButtonGlow, isSelected && styles.playButtonGlowActive]} />
|
<View style={[styles.playButtonGlow, isSelected && styles.playButtonGlowActive]} />
|
||||||
|
|
||||||
{/* Main circle button */}
|
{/* Main circle button - premium dark with primary gradient border */}
|
||||||
<Pressable onPress={() => globalMenuToggle?.()}>
|
<Pressable onPress={() => globalMenuToggle?.()}>
|
||||||
<Animated.View style={styles.playButtonCircle}>
|
<Animated.View style={styles.playButtonCircle}>
|
||||||
|
<View style={styles.playButtonInner}>
|
||||||
|
{/* Silk highlight at top - primary gradient */}
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={[`${NAV_COLORS.primaryRgba}0.2)`, `${NAV_COLORS.secondaryRgba}0.1)`, 'transparent']}
|
||||||
|
style={styles.playButtonHighlight}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
style={styles.playButtonGradient}
|
/>
|
||||||
>
|
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={globalMenuOpen ? "close" : "flame"}
|
name={globalMenuOpen ? "close" : "flame"}
|
||||||
size={32}
|
size={28}
|
||||||
color={colors.text}
|
color={globalMenuOpen ? colors.textSecondary : NAV_COLORS.primary}
|
||||||
/>
|
/>
|
||||||
</LinearGradient>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
@@ -524,8 +541,8 @@ export default function AppLayout() {
|
|||||||
tabBarBackground: () => <TabBarBackground />,
|
tabBarBackground: () => <TabBarBackground />,
|
||||||
tabBarStyle: shouldHideTabBar ? { display: 'none' } : {
|
tabBarStyle: shouldHideTabBar ? { display: 'none' } : {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
backgroundColor: 'rgba(18, 18, 18, 0.85)',
|
backgroundColor: 'rgba(13, 13, 26, 0.92)',
|
||||||
borderTopColor: colors.glass.border,
|
borderTopColor: 'rgba(233, 69, 96, 0.2)', // primary brand color
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
paddingBottom: Platform.OS === 'ios' ? 28 : 8,
|
paddingBottom: Platform.OS === 'ios' ? 28 : 8,
|
||||||
@@ -533,10 +550,11 @@ export default function AppLayout() {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
},
|
},
|
||||||
tabBarActiveTintColor: colors.primary,
|
tabBarActiveTintColor: colors.primary,
|
||||||
tabBarInactiveTintColor: '#fff',
|
tabBarInactiveTintColor: colors.textSecondary,
|
||||||
tabBarLabelStyle: {
|
tabBarLabelStyle: {
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
fontWeight: "600",
|
fontWeight: "500",
|
||||||
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -574,9 +592,12 @@ export default function AppLayout() {
|
|||||||
),
|
),
|
||||||
tabBarBadge: (newMatchesCount + unreadCount) > 0 ? (newMatchesCount + unreadCount) : undefined,
|
tabBarBadge: (newMatchesCount + unreadCount) > 0 ? (newMatchesCount + unreadCount) : undefined,
|
||||||
tabBarBadgeStyle: {
|
tabBarBadgeStyle: {
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.premium.rose,
|
||||||
fontSize: 10,
|
fontSize: 9,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
|
minWidth: 16,
|
||||||
|
height: 16,
|
||||||
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -658,6 +679,43 @@ export default function AppLayout() {
|
|||||||
outputRange: [0, translateY],
|
outputRange: [0, translateY],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Brand and feature variant colors
|
||||||
|
const getVariantStyles = () => {
|
||||||
|
switch (item.variant) {
|
||||||
|
case 'primary':
|
||||||
|
return {
|
||||||
|
bg: `${NAV_COLORS.primaryRgba}0.15)`,
|
||||||
|
border: `${NAV_COLORS.primaryRgba}0.35)`,
|
||||||
|
icon: NAV_COLORS.primary,
|
||||||
|
label: NAV_COLORS.primary,
|
||||||
|
};
|
||||||
|
case 'gold':
|
||||||
|
return {
|
||||||
|
bg: `${NAV_COLORS.goldRgba}0.15)`,
|
||||||
|
border: `${NAV_COLORS.goldRgba}0.35)`,
|
||||||
|
icon: NAV_COLORS.gold,
|
||||||
|
label: NAV_COLORS.gold,
|
||||||
|
};
|
||||||
|
case 'rose':
|
||||||
|
return {
|
||||||
|
bg: `${NAV_COLORS.roseRgba}0.15)`,
|
||||||
|
border: `${NAV_COLORS.roseRgba}0.35)`,
|
||||||
|
icon: NAV_COLORS.rose,
|
||||||
|
label: NAV_COLORS.rose,
|
||||||
|
};
|
||||||
|
case 'muted':
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
bg: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
border: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
icon: colors.textSecondary,
|
||||||
|
label: colors.textSecondary,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const variantStyles = getVariantStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -678,15 +736,20 @@ export default function AppLayout() {
|
|||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
radialStyles.menuItemCircle,
|
radialStyles.menuItemCircle,
|
||||||
item.color ? { backgroundColor: item.color } : null,
|
{
|
||||||
|
backgroundColor: variantStyles.bg,
|
||||||
|
borderColor: variantStyles.border,
|
||||||
|
},
|
||||||
]}>
|
]}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={item.icon}
|
name={item.icon}
|
||||||
size={22}
|
size={22}
|
||||||
color={colors.text}
|
color={variantStyles.icon}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={radialStyles.menuItemLabel}>{item.label}</Text>
|
<Text style={[radialStyles.menuItemLabel, { color: variantStyles.label }]}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
@@ -890,7 +953,7 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
// Play button styles
|
// Play button styles - Premium boutique
|
||||||
playButtonContainer: {
|
playButtonContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -899,23 +962,38 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
playButtonGlow: {
|
playButtonGlow: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: 72,
|
width: 76,
|
||||||
height: 72,
|
height: 76,
|
||||||
borderRadius: 36,
|
borderRadius: 38,
|
||||||
backgroundColor: colors.primaryGlow,
|
backgroundColor: colors.primaryGlow,
|
||||||
opacity: 0.5,
|
opacity: 0.4,
|
||||||
},
|
},
|
||||||
playButtonGlowActive: {
|
playButtonGlowActive: {
|
||||||
opacity: 1,
|
opacity: 0.7,
|
||||||
...shadows.glow(colors.primary),
|
...shadows.glow(colors.primary),
|
||||||
},
|
},
|
||||||
playButtonCircle: {
|
playButtonCircle: {
|
||||||
width: 60,
|
width: 64,
|
||||||
height: 60,
|
height: 64,
|
||||||
borderRadius: 30,
|
borderRadius: 32,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: 'rgba(233, 69, 96, 0.4)', // primary brand
|
||||||
...shadows.lg,
|
...shadows.lg,
|
||||||
},
|
},
|
||||||
|
playButtonInner: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#0d0d1a',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
playButtonHighlight: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: '50%',
|
||||||
|
},
|
||||||
playButtonGradient: {
|
playButtonGradient: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -923,13 +1001,13 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Radial menu styles
|
// Radial menu styles - Premium boutique
|
||||||
const TAB_BAR_HEIGHT = Platform.OS === 'ios' ? 88 : 64;
|
const TAB_BAR_HEIGHT = Platform.OS === 'ios' ? 88 : 64;
|
||||||
|
|
||||||
const radialStyles = StyleSheet.create({
|
const radialStyles = StyleSheet.create({
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
backgroundColor: 'rgba(13, 13, 26, 0.85)',
|
||||||
zIndex: 998,
|
zIndex: 998,
|
||||||
},
|
},
|
||||||
menuContainer: {
|
menuContainer: {
|
||||||
@@ -948,21 +1026,23 @@ const radialStyles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
menuItemCircle: {
|
menuItemCircle: {
|
||||||
width: 52,
|
width: 56,
|
||||||
height: 52,
|
height: 56,
|
||||||
borderRadius: 26,
|
borderRadius: 28,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: 'rgba(22, 33, 62, 0.9)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
...shadows.lg,
|
...shadows.md,
|
||||||
borderWidth: 2,
|
borderWidth: 1.5,
|
||||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
borderColor: 'rgba(255, 255, 255, 0.15)',
|
||||||
},
|
},
|
||||||
menuItemLabel: {
|
menuItemLabel: {
|
||||||
...typography.caption1,
|
...typography.caption2,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginTop: 6,
|
marginTop: 8,
|
||||||
fontWeight: '600',
|
fontWeight: '500',
|
||||||
|
letterSpacing: 1,
|
||||||
|
textTransform: 'uppercase',
|
||||||
textShadowColor: 'rgba(0, 0, 0, 0.5)',
|
textShadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
textShadowOffset: { width: 0, height: 1 },
|
textShadowOffset: { width: 0, height: 1 },
|
||||||
textShadowRadius: 2,
|
textShadowRadius: 2,
|
||||||
|
|||||||
+1379
-190
File diff suppressed because it is too large.
Load diff
@@ -33,11 +33,6 @@ export default function DaresScreen() {
|
|||||||
entering={FadeInDown.duration(600).springify()}
|
entering={FadeInDown.duration(600).springify()}
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
|
||||||
<View style={styles.iconContainer}>
|
|
||||||
<Ionicons name="flame" size={36} color={ACCENT_COLOR} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Title section */}
|
{/* Title section */}
|
||||||
<Text style={styles.label}>ADVENTURE</Text>
|
<Text style={styles.label}>ADVENTURE</Text>
|
||||||
<Text style={styles.title}>Dares</Text>
|
<Text style={styles.title}>Dares</Text>
|
||||||
@@ -81,12 +76,10 @@ export default function DaresScreen() {
|
|||||||
{/* Bottom teaser */}
|
{/* Bottom teaser */}
|
||||||
<Text style={styles.teaser}>Are you ready to play?</Text>
|
<Text style={styles.teaser}>Are you ready to play?</Text>
|
||||||
|
|
||||||
{/* Interest button */}
|
{/* Interest button - always reserve space to prevent layout shift */}
|
||||||
|
<View style={styles.interestButtonContainer}>
|
||||||
{isAuthenticated && !isLoading && (
|
{isAuthenticated && !isLoading && (
|
||||||
<Animated.View
|
<Animated.View entering={FadeIn.delay(500).duration(400)}>
|
||||||
entering={FadeIn.delay(500).duration(400)}
|
|
||||||
style={styles.interestButtonContainer}
|
|
||||||
>
|
|
||||||
<AnimatedPressable
|
<AnimatedPressable
|
||||||
onPress={toggleInterest}
|
onPress={toggleInterest}
|
||||||
onPressIn={handlePressIn}
|
onPressIn={handlePressIn}
|
||||||
@@ -115,6 +108,7 @@ export default function DaresScreen() {
|
|||||||
</AnimatedPressable>
|
</AnimatedPressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</GradientBackground>
|
</GradientBackground>
|
||||||
@@ -127,7 +121,7 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingTop: Platform.OS === 'ios' ? 60 : 40,
|
paddingTop: 60,
|
||||||
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
@@ -135,17 +129,6 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
},
|
},
|
||||||
iconContainer: {
|
|
||||||
width: 72,
|
|
||||||
height: 72,
|
|
||||||
borderRadius: 36,
|
|
||||||
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(212, 175, 55, 0.2)',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: spacing.xl,
|
|
||||||
},
|
|
||||||
label: {
|
label: {
|
||||||
...typography.caption1,
|
...typography.caption1,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
@@ -223,6 +206,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
interestButtonContainer: {
|
interestButtonContainer: {
|
||||||
marginTop: spacing.xl,
|
marginTop: spacing.xl,
|
||||||
|
minHeight: 44,
|
||||||
},
|
},
|
||||||
interestButton: {
|
interestButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
+207
-148
@@ -3,6 +3,7 @@ import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, useWind
|
|||||||
import Animated, {
|
import Animated, {
|
||||||
FadeInDown,
|
FadeInDown,
|
||||||
FadeInRight,
|
FadeInRight,
|
||||||
|
FadeIn,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
withRepeat,
|
withRepeat,
|
||||||
@@ -17,6 +18,7 @@ import { GradientBackground, GlassCard } from "../../src/components/ui";
|
|||||||
import { colors, gradients, spacing, radius, typography, shadows } from "../../src/theme";
|
import { colors, gradients, spacing, radius, typography, shadows } from "../../src/theme";
|
||||||
|
|
||||||
const MAX_CONTENT_WIDTH = 500;
|
const MAX_CONTENT_WIDTH = 500;
|
||||||
|
const ACCENT_COLOR = colors.primary;
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { user, partner, couple } = useAuthStore();
|
const { user, partner, couple } = useAuthStore();
|
||||||
@@ -35,12 +37,18 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
// Subtle pulsating animation for the CTA icon
|
// Subtle pulsating animation for the CTA icon
|
||||||
const pulseScale = useSharedValue(1);
|
const pulseScale = useSharedValue(1);
|
||||||
|
const glowOpacity = useSharedValue(0.3);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const startPulse = () => {
|
const startPulse = () => {
|
||||||
pulseScale.value = 1;
|
pulseScale.value = 1;
|
||||||
pulseScale.value = withRepeat(
|
pulseScale.value = withRepeat(
|
||||||
withTiming(1.1, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
|
withTiming(1.1, { duration: 1500, easing: Easing.inOut(Easing.ease) }),
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
glowOpacity.value = withRepeat(
|
||||||
|
withTiming(0.6, { duration: 1500, easing: Easing.inOut(Easing.ease) }),
|
||||||
-1,
|
-1,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -49,11 +57,13 @@ export default function HomeScreen() {
|
|||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pulseAnimatedStyle = useAnimatedStyle(() => {
|
const pulseAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
return {
|
|
||||||
transform: [{ scale: pulseScale.value }],
|
transform: [{ scale: pulseScale.value }],
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
|
const glowAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
|
opacity: glowOpacity.value,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientBackground>
|
<GradientBackground>
|
||||||
@@ -66,28 +76,41 @@ export default function HomeScreen() {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={[styles.innerContainer, isWideScreen && styles.innerContainerWide]}>
|
<View style={[styles.innerContainer, isWideScreen && styles.innerContainerWide]}>
|
||||||
{/* Header */}
|
{/* Premium Header */}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInDown.delay(100).duration(500)}
|
entering={FadeInDown.delay(100).duration(600).springify()}
|
||||||
style={styles.header}
|
style={styles.header}
|
||||||
>
|
>
|
||||||
<View style={styles.headerLeft}>
|
{/* Greeting section */}
|
||||||
|
<Text style={styles.label}>WELCOME BACK</Text>
|
||||||
<Text style={styles.greeting}>
|
<Text style={styles.greeting}>
|
||||||
Hey, {user?.name || "there"}
|
{user?.name || "Beautiful"}
|
||||||
</Text>
|
|
||||||
{partner ? (
|
|
||||||
<View style={styles.partnerBadge}>
|
|
||||||
<Ionicons name="heart" size={12} color={colors.primary} />
|
|
||||||
<Text style={styles.partnerText}>
|
|
||||||
{partner.name || partner.email?.split('@')[0] || 'Partner'}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Partner badge */}
|
||||||
|
{partner ? (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.delay(300).duration(400)}
|
||||||
|
style={styles.partnerBadge}
|
||||||
|
>
|
||||||
|
<Ionicons name="heart" size={12} color={ACCENT_COLOR} />
|
||||||
|
<Text style={styles.partnerText}>
|
||||||
|
Connected with {partner.name || partner.email?.split('@')[0] || 'Partner'}
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
) : couple ? (
|
) : couple ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.waitingBadge}
|
style={styles.waitingBadge}
|
||||||
onPress={() => router.push("/(app)/pairing")}
|
onPress={() => router.push("/(app)/pairing")}
|
||||||
>
|
>
|
||||||
<Ionicons name="hourglass-outline" size={12} color={colors.primary} />
|
<Ionicons name="hourglass-outline" size={12} color={ACCENT_COLOR} />
|
||||||
<Text style={styles.waitingText}>Waiting for partner</Text>
|
<Text style={styles.waitingText}>Waiting for partner</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
@@ -96,27 +119,10 @@ export default function HomeScreen() {
|
|||||||
onPress={() => router.push("/(app)/pairing")}
|
onPress={() => router.push("/(app)/pairing")}
|
||||||
>
|
>
|
||||||
<Ionicons name="add-circle-outline" size={12} color={colors.textTertiary} />
|
<Ionicons name="add-circle-outline" size={12} color={colors.textTertiary} />
|
||||||
<Text style={styles.pairText}>Pair with partner</Text>
|
<Text style={styles.pairText}>Connect with your partner</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => router.push("/(app)/profile")}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={gradients.primary as [string, string]}
|
|
||||||
style={styles.avatarGradient}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
>
|
|
||||||
<View style={styles.avatarInner}>
|
|
||||||
<Text style={styles.avatarText}>
|
|
||||||
{user?.name?.[0]?.toUpperCase() || "U"}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* Stats Row */}
|
{/* Stats Row */}
|
||||||
@@ -129,24 +135,17 @@ export default function HomeScreen() {
|
|||||||
onPress={() => router.push("/(app)/matches")}
|
onPress={() => router.push("/(app)/matches")}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<GlassCard>
|
<View style={[styles.statCard, matches.length > 0 && styles.statCardActive]}>
|
||||||
<View style={styles.statContent}>
|
<View style={[styles.statIcon, matches.length > 0 && styles.statIconActive]}>
|
||||||
<LinearGradient
|
|
||||||
colors={matches.length > 0 ? gradients.primary as [string, string] : [colors.glass.background, colors.glass.background]}
|
|
||||||
style={styles.statIcon}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
>
|
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="heart"
|
name="heart"
|
||||||
size={18}
|
size={18}
|
||||||
color={matches.length > 0 ? colors.text : colors.primary}
|
color={matches.length > 0 ? colors.text : ACCENT_COLOR}
|
||||||
/>
|
/>
|
||||||
</LinearGradient>
|
</View>
|
||||||
<Text style={styles.statNumber}>{matches.length}</Text>
|
<Text style={styles.statNumber}>{matches.length}</Text>
|
||||||
<Text style={styles.statLabel}>Matches</Text>
|
<Text style={styles.statLabel}>Matches</Text>
|
||||||
</View>
|
</View>
|
||||||
</GlassCard>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -154,24 +153,17 @@ export default function HomeScreen() {
|
|||||||
onPress={() => router.push("/(app)/packs")}
|
onPress={() => router.push("/(app)/packs")}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<GlassCard>
|
<View style={[styles.statCard, enabledPacksCount > 0 && styles.statCardActive]}>
|
||||||
<View style={styles.statContent}>
|
<View style={[styles.statIcon, enabledPacksCount > 0 && styles.statIconActive]}>
|
||||||
<LinearGradient
|
|
||||||
colors={enabledPacksCount > 0 ? gradients.primary as [string, string] : [colors.glass.background, colors.glass.background]}
|
|
||||||
style={styles.statIcon}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
>
|
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="layers"
|
name="layers"
|
||||||
size={18}
|
size={18}
|
||||||
color={enabledPacksCount > 0 ? colors.text : colors.primary}
|
color={enabledPacksCount > 0 ? colors.text : ACCENT_COLOR}
|
||||||
/>
|
/>
|
||||||
</LinearGradient>
|
</View>
|
||||||
<Text style={styles.statNumber}>{enabledPacksCount}</Text>
|
<Text style={styles.statNumber}>{enabledPacksCount}</Text>
|
||||||
<Text style={styles.statLabel}>Packs</Text>
|
<Text style={styles.statLabel}>Packs</Text>
|
||||||
</View>
|
</View>
|
||||||
</GlassCard>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -179,24 +171,17 @@ export default function HomeScreen() {
|
|||||||
onPress={() => router.push("/(app)/matches")}
|
onPress={() => router.push("/(app)/matches")}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<GlassCard>
|
<View style={[styles.statCard, newMatchesCount > 0 && styles.statCardActive]}>
|
||||||
<View style={styles.statContent}>
|
<View style={[styles.statIcon, newMatchesCount > 0 && styles.statIconActive]}>
|
||||||
<LinearGradient
|
|
||||||
colors={newMatchesCount > 0 ? gradients.primary as [string, string] : [colors.glass.background, colors.glass.background]}
|
|
||||||
style={styles.statIcon}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
>
|
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="sparkles"
|
name="sparkles"
|
||||||
size={18}
|
size={18}
|
||||||
color={newMatchesCount > 0 ? colors.text : colors.primary}
|
color={newMatchesCount > 0 ? colors.text : ACCENT_COLOR}
|
||||||
/>
|
/>
|
||||||
</LinearGradient>
|
</View>
|
||||||
<Text style={styles.statNumber}>{newMatchesCount}</Text>
|
<Text style={styles.statNumber}>{newMatchesCount}</Text>
|
||||||
<Text style={styles.statLabel}>New</Text>
|
<Text style={styles.statLabel}>New</Text>
|
||||||
</View>
|
</View>
|
||||||
</GlassCard>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
@@ -209,22 +194,28 @@ export default function HomeScreen() {
|
|||||||
onPress={() => router.push("/(app)/swipe")}
|
onPress={() => router.push("/(app)/swipe")}
|
||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
>
|
>
|
||||||
|
<View style={styles.ctaCard}>
|
||||||
|
{/* Glow effect */}
|
||||||
|
<Animated.View style={[styles.ctaGlow, glowAnimatedStyle]} />
|
||||||
|
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={gradients.primary as [string, string]}
|
||||||
style={styles.ctaCard}
|
style={styles.ctaGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
>
|
>
|
||||||
<View style={styles.ctaContent}>
|
<View style={styles.ctaContent}>
|
||||||
|
<Text style={styles.ctaLabel}>EXPLORE</Text>
|
||||||
<Text style={styles.ctaTitle}>Ready to explore?</Text>
|
<Text style={styles.ctaTitle}>Ready to explore?</Text>
|
||||||
<Text style={styles.ctaSubtitle}>
|
<Text style={styles.ctaSubtitle}>
|
||||||
Swipe through questions and discover what you both enjoy
|
Discover new experiences
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Animated.View style={[styles.ctaIconContainer, pulseAnimatedStyle]}>
|
<Animated.View style={[styles.ctaIconContainer, pulseAnimatedStyle]}>
|
||||||
<Ionicons name="flame" size={24} color={colors.text} />
|
<Ionicons name="flame" size={28} color={colors.text} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
@@ -235,13 +226,18 @@ export default function HomeScreen() {
|
|||||||
style={styles.section}
|
style={styles.section}
|
||||||
>
|
>
|
||||||
<View style={styles.sectionHeader}>
|
<View style={styles.sectionHeader}>
|
||||||
|
<View style={styles.sectionTitleRow}>
|
||||||
|
<View style={styles.sectionIcon}>
|
||||||
|
<Ionicons name="heart" size={14} color={ACCENT_COLOR} />
|
||||||
|
</View>
|
||||||
<Text style={styles.sectionTitle}>Recent Matches</Text>
|
<Text style={styles.sectionTitle}>Recent Matches</Text>
|
||||||
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push("/(app)/matches")}
|
onPress={() => router.push("/(app)/matches")}
|
||||||
style={styles.seeAllButton}
|
style={styles.seeAllButton}
|
||||||
>
|
>
|
||||||
<Text style={styles.seeAllText}>See all</Text>
|
<Text style={styles.seeAllText}>See all</Text>
|
||||||
<Ionicons name="chevron-forward" size={14} color={colors.primary} />
|
<Ionicons name="chevron-forward" size={14} color={ACCENT_COLOR} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
{recentMatches.map((match, index) => (
|
{recentMatches.map((match, index) => (
|
||||||
@@ -253,7 +249,7 @@ export default function HomeScreen() {
|
|||||||
onPress={() => router.push(`/chat/${match.id}`)}
|
onPress={() => router.push(`/chat/${match.id}`)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<GlassCard style={styles.matchCard}>
|
<View style={styles.matchCard}>
|
||||||
<View style={styles.matchRow}>
|
<View style={styles.matchRow}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={gradients.primary as [string, string]}
|
||||||
@@ -261,14 +257,14 @@ export default function HomeScreen() {
|
|||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
>
|
>
|
||||||
<Ionicons name="heart" size={16} color={colors.text} />
|
<Ionicons name="heart" size={14} color={colors.text} />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
<Text style={styles.matchText} numberOfLines={1}>
|
<Text style={styles.matchText} numberOfLines={1}>
|
||||||
{(match as any).question?.text || "A new match!"}
|
{(match as any).question?.text || "A new match!"}
|
||||||
</Text>
|
</Text>
|
||||||
{match.is_new && <View style={styles.newBadge} />}
|
{match.is_new && <View style={styles.newBadge} />}
|
||||||
</View>
|
</View>
|
||||||
</GlassCard>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
))}
|
))}
|
||||||
@@ -281,40 +277,48 @@ export default function HomeScreen() {
|
|||||||
entering={FadeInDown.delay(400).duration(500)}
|
entering={FadeInDown.delay(400).duration(500)}
|
||||||
style={styles.section}
|
style={styles.section}
|
||||||
>
|
>
|
||||||
<Text style={styles.tipsTitle}>Getting Started</Text>
|
<View style={styles.sectionHeader}>
|
||||||
<GlassCard>
|
<View style={styles.sectionTitleRow}>
|
||||||
|
<View style={styles.sectionIcon}>
|
||||||
|
<Ionicons name="compass" size={14} color={ACCENT_COLOR} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.sectionTitle}>Getting Started</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.tipCard}>
|
||||||
<View style={styles.tipRow}>
|
<View style={styles.tipRow}>
|
||||||
<View style={styles.tipIcon}>
|
<View style={styles.tipIcon}>
|
||||||
<Ionicons name="layers-outline" size={18} color={colors.primary} />
|
<Ionicons name="layers-outline" size={18} color={ACCENT_COLOR} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.tipContent}>
|
<View style={styles.tipContent}>
|
||||||
<Text style={styles.tipText}>Enable question packs</Text>
|
<Text style={styles.tipText}>Enable question packs</Text>
|
||||||
<Text style={styles.tipSubtext}>Choose topics you want to explore</Text>
|
<Text style={styles.tipSubtext}>Choose topics to explore together</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</GlassCard>
|
</View>
|
||||||
<GlassCard style={styles.tipCard}>
|
<View style={[styles.tipCard, styles.tipCardSpaced]}>
|
||||||
<View style={styles.tipRow}>
|
<View style={styles.tipRow}>
|
||||||
<View style={styles.tipIcon}>
|
<View style={styles.tipIcon}>
|
||||||
<Ionicons name="swap-horizontal-outline" size={18} color={colors.primary} />
|
<Ionicons name="swap-horizontal-outline" size={18} color={ACCENT_COLOR} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.tipContent}>
|
<View style={styles.tipContent}>
|
||||||
<Text style={styles.tipText}>Swipe on questions</Text>
|
<Text style={styles.tipText}>Swipe on questions</Text>
|
||||||
<Text style={styles.tipSubtext}>Answer yes, no, or maybe</Text>
|
<Text style={styles.tipSubtext}>Answer yes, no, or maybe</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</GlassCard>
|
</View>
|
||||||
<GlassCard style={styles.tipCard}>
|
<View style={[styles.tipCard, styles.tipCardSpaced]}>
|
||||||
<View style={styles.tipRow}>
|
<View style={styles.tipRow}>
|
||||||
<View style={styles.tipIcon}>
|
<View style={styles.tipIcon}>
|
||||||
<Ionicons name="heart-outline" size={18} color={colors.primary} />
|
<Ionicons name="heart-outline" size={18} color={ACCENT_COLOR} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.tipContent}>
|
<View style={styles.tipContent}>
|
||||||
<Text style={styles.tipText}>Discover matches</Text>
|
<Text style={styles.tipText}>Discover matches</Text>
|
||||||
<Text style={styles.tipSubtext}>See where you both agree</Text>
|
<Text style={styles.tipSubtext}>See where you both agree</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</GlassCard>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -346,27 +350,51 @@ const styles = StyleSheet.create({
|
|||||||
header: {
|
header: {
|
||||||
paddingTop: 60,
|
paddingTop: 60,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingBottom: spacing.lg,
|
paddingBottom: spacing.xl,
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
headerLeft: {
|
label: {
|
||||||
flex: 1,
|
...typography.caption2,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 2.5,
|
||||||
|
color: ACCENT_COLOR,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xs,
|
||||||
},
|
},
|
||||||
greeting: {
|
greeting: {
|
||||||
...typography.title1,
|
...typography.largeTitle,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
separatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(233, 69, 96, 0.3)',
|
||||||
|
},
|
||||||
|
separatorDiamond: {
|
||||||
|
width: 5,
|
||||||
|
height: 5,
|
||||||
|
backgroundColor: ACCENT_COLOR,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.sm,
|
||||||
|
opacity: 0.6,
|
||||||
},
|
},
|
||||||
partnerBadge: {
|
partnerBadge: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.primaryLight,
|
backgroundColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.sm,
|
||||||
borderRadius: radius.full,
|
borderRadius: radius.full,
|
||||||
alignSelf: 'flex-start',
|
borderWidth: 1,
|
||||||
marginTop: spacing.sm,
|
borderColor: 'rgba(233, 69, 96, 0.2)',
|
||||||
gap: spacing.xs,
|
gap: spacing.xs,
|
||||||
},
|
},
|
||||||
partnerText: {
|
partnerText: {
|
||||||
@@ -378,11 +406,11 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.glass.background,
|
backgroundColor: colors.glass.background,
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.sm,
|
||||||
borderRadius: radius.full,
|
borderRadius: radius.full,
|
||||||
alignSelf: 'flex-start',
|
borderWidth: 1,
|
||||||
marginTop: spacing.sm,
|
borderColor: colors.glass.border,
|
||||||
gap: spacing.xs,
|
gap: spacing.xs,
|
||||||
},
|
},
|
||||||
pairText: {
|
pairText: {
|
||||||
@@ -392,12 +420,12 @@ const styles = StyleSheet.create({
|
|||||||
waitingBadge: {
|
waitingBadge: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.primaryLight,
|
backgroundColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.sm,
|
||||||
borderRadius: radius.full,
|
borderRadius: radius.full,
|
||||||
alignSelf: 'flex-start',
|
borderWidth: 1,
|
||||||
marginTop: spacing.sm,
|
borderColor: 'rgba(233, 69, 96, 0.2)',
|
||||||
gap: spacing.xs,
|
gap: spacing.xs,
|
||||||
},
|
},
|
||||||
waitingText: {
|
waitingText: {
|
||||||
@@ -405,26 +433,6 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.text,
|
color: colors.text,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
avatarGradient: {
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
borderRadius: 24,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
...shadows.md,
|
|
||||||
},
|
|
||||||
avatarInner: {
|
|
||||||
width: 42,
|
|
||||||
height: 42,
|
|
||||||
borderRadius: 21,
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
avatarText: {
|
|
||||||
...typography.headline,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
statsContainer: {
|
statsContainer: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
@@ -433,18 +441,30 @@ const styles = StyleSheet.create({
|
|||||||
statCardWrapper: {
|
statCardWrapper: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
statContent: {
|
statCard: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.md,
|
||||||
|
backgroundColor: 'rgba(233, 69, 96, 0.05)',
|
||||||
|
borderRadius: radius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
|
},
|
||||||
|
statCardActive: {
|
||||||
|
backgroundColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
|
borderColor: 'rgba(233, 69, 96, 0.2)',
|
||||||
},
|
},
|
||||||
statIcon: {
|
statIcon: {
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
|
backgroundColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
},
|
},
|
||||||
|
statIconActive: {
|
||||||
|
backgroundColor: ACCENT_COLOR,
|
||||||
|
},
|
||||||
statNumber: {
|
statNumber: {
|
||||||
...typography.title2,
|
...typography.title2,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
@@ -456,9 +476,23 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
ctaSection: {
|
ctaSection: {
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
marginTop: spacing.lg,
|
marginTop: spacing.xl,
|
||||||
},
|
},
|
||||||
ctaCard: {
|
ctaCard: {
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: radius.xl,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
ctaGlow: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -20,
|
||||||
|
left: -20,
|
||||||
|
right: -20,
|
||||||
|
bottom: -20,
|
||||||
|
backgroundColor: ACCENT_COLOR,
|
||||||
|
borderRadius: radius.xl + 20,
|
||||||
|
},
|
||||||
|
ctaGradient: {
|
||||||
borderRadius: radius.xl,
|
borderRadius: radius.xl,
|
||||||
padding: spacing.lg,
|
padding: spacing.lg,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -468,21 +502,29 @@ const styles = StyleSheet.create({
|
|||||||
ctaContent: {
|
ctaContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
ctaLabel: {
|
||||||
|
...typography.caption2,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: 'rgba(255,255,255,0.8)',
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
ctaTitle: {
|
ctaTitle: {
|
||||||
...typography.title3,
|
...typography.title2,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: spacing.xs,
|
marginBottom: spacing.xs,
|
||||||
},
|
},
|
||||||
ctaSubtitle: {
|
ctaSubtitle: {
|
||||||
...typography.subhead,
|
...typography.subhead,
|
||||||
color: "rgba(255,255,255,0.8)",
|
color: "rgba(255,255,255,0.8)",
|
||||||
lineHeight: 20,
|
|
||||||
},
|
},
|
||||||
ctaIconContainer: {
|
ctaIconContainer: {
|
||||||
width: 48,
|
width: 56,
|
||||||
height: 48,
|
height: 56,
|
||||||
borderRadius: 24,
|
borderRadius: 28,
|
||||||
backgroundColor: "rgba(255,255,255,0.2)",
|
backgroundColor: "rgba(255,255,255,0.2)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255,255,255,0.3)',
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginLeft: spacing.md,
|
marginLeft: spacing.md,
|
||||||
@@ -497,6 +539,19 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
},
|
},
|
||||||
|
sectionTitleRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
sectionIcon: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
...typography.headline,
|
...typography.headline,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
@@ -508,19 +563,24 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
seeAllText: {
|
seeAllText: {
|
||||||
...typography.subhead,
|
...typography.subhead,
|
||||||
color: colors.primary,
|
color: ACCENT_COLOR,
|
||||||
},
|
},
|
||||||
matchCard: {
|
matchCard: {
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
|
backgroundColor: 'rgba(233, 69, 96, 0.05)',
|
||||||
|
borderRadius: radius.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
|
padding: spacing.md,
|
||||||
},
|
},
|
||||||
matchRow: {
|
matchRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
matchIcon: {
|
matchIcon: {
|
||||||
width: 32,
|
width: 28,
|
||||||
height: 32,
|
height: 28,
|
||||||
borderRadius: 16,
|
borderRadius: 14,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginRight: spacing.md,
|
marginRight: spacing.md,
|
||||||
@@ -534,17 +594,16 @@ const styles = StyleSheet.create({
|
|||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: ACCENT_COLOR,
|
||||||
},
|
|
||||||
tipsTitle: {
|
|
||||||
...typography.caption1,
|
|
||||||
color: colors.textTertiary,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 1.5,
|
|
||||||
marginBottom: spacing.sm,
|
|
||||||
marginLeft: spacing.xs,
|
|
||||||
},
|
},
|
||||||
tipCard: {
|
tipCard: {
|
||||||
|
backgroundColor: 'rgba(233, 69, 96, 0.05)',
|
||||||
|
borderRadius: radius.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
|
padding: spacing.md,
|
||||||
|
},
|
||||||
|
tipCardSpaced: {
|
||||||
marginTop: spacing.sm,
|
marginTop: spacing.sm,
|
||||||
},
|
},
|
||||||
tipRow: {
|
tipRow: {
|
||||||
@@ -555,7 +614,7 @@ const styles = StyleSheet.create({
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
backgroundColor: colors.primaryLight,
|
backgroundColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: spacing.md,
|
marginRight: spacing.md,
|
||||||
|
|||||||
+414
-142
@@ -5,12 +5,30 @@ import { useFocusEffect } from "expo-router";
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import Animated, { FadeInDown, FadeInRight } from "react-native-reanimated";
|
import Animated, {
|
||||||
import { GradientBackground, GlassCard } from "../../src/components/ui";
|
FadeIn,
|
||||||
|
FadeInDown,
|
||||||
|
FadeInRight,
|
||||||
|
FadeInUp,
|
||||||
|
useSharedValue,
|
||||||
|
useAnimatedStyle,
|
||||||
|
withRepeat,
|
||||||
|
withSequence,
|
||||||
|
withTiming,
|
||||||
|
interpolate,
|
||||||
|
Easing,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui";
|
||||||
import { colors, gradients, spacing, typography, radius, shadows } from "../../src/theme";
|
import { colors, gradients, spacing, typography, radius, shadows } from "../../src/theme";
|
||||||
import MatchesTutorial from "../../src/components/MatchesTutorial";
|
import MatchesTutorial from "../../src/components/MatchesTutorial";
|
||||||
import { hasSeenMatchesTutorial, markMatchesTutorialSeen } from "../../src/lib/matchesTutorialSeen";
|
import { hasSeenMatchesTutorial, markMatchesTutorialSeen } from "../../src/lib/matchesTutorialSeen";
|
||||||
|
|
||||||
|
// Premium color palette
|
||||||
|
const ACCENT = colors.premium.gold;
|
||||||
|
const ACCENT_RGBA = 'rgba(212, 175, 55, ';
|
||||||
|
const ROSE = colors.premium.rose;
|
||||||
|
const ROSE_RGBA = 'rgba(232, 164, 174, ';
|
||||||
|
|
||||||
const MAX_CONTENT_WIDTH = 500;
|
const MAX_CONTENT_WIDTH = 500;
|
||||||
|
|
||||||
export default function MatchesScreen() {
|
export default function MatchesScreen() {
|
||||||
@@ -21,6 +39,60 @@ export default function MatchesScreen() {
|
|||||||
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
||||||
const [showTutorial, setShowTutorial] = useState(false);
|
const [showTutorial, setShowTutorial] = useState(false);
|
||||||
|
|
||||||
|
// Ambient orb breathing animations
|
||||||
|
const orbBreathing1 = useSharedValue(0);
|
||||||
|
const orbBreathing2 = useSharedValue(0);
|
||||||
|
const orbDrift = useSharedValue(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Primary orb breathing - 6 second cycle
|
||||||
|
orbBreathing1.value = withRepeat(
|
||||||
|
withSequence(
|
||||||
|
withTiming(1, { duration: 3000, easing: Easing.inOut(Easing.sin) }),
|
||||||
|
withTiming(0, { duration: 3000, easing: Easing.inOut(Easing.sin) })
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Secondary orb breathing - offset timing for variation
|
||||||
|
orbBreathing2.value = withRepeat(
|
||||||
|
withSequence(
|
||||||
|
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.sin) }),
|
||||||
|
withTiming(1, { duration: 4000, easing: Easing.inOut(Easing.sin) }),
|
||||||
|
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.sin) })
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subtle vertical drift - 8 second cycle
|
||||||
|
orbDrift.value = withRepeat(
|
||||||
|
withSequence(
|
||||||
|
withTiming(1, { duration: 4000, easing: Easing.inOut(Easing.sin) }),
|
||||||
|
withTiming(0, { duration: 4000, easing: Easing.inOut(Easing.sin) })
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const orbStyle1 = useAnimatedStyle(() => ({
|
||||||
|
opacity: interpolate(orbBreathing1.value, [0, 1], [0.25, 0.5]),
|
||||||
|
transform: [
|
||||||
|
{ translateY: interpolate(orbDrift.value, [0, 1], [0, -20]) },
|
||||||
|
{ scale: interpolate(orbBreathing1.value, [0, 1], [1, 1.1]) },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const orbStyle2 = useAnimatedStyle(() => ({
|
||||||
|
opacity: interpolate(orbBreathing2.value, [0, 1], [0.2, 0.4]),
|
||||||
|
transform: [
|
||||||
|
{ translateY: interpolate(orbDrift.value, [0, 1], [20, 0]) },
|
||||||
|
{ scale: interpolate(orbBreathing2.value, [0, 1], [1, 1.1]) },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
// Check if tutorial should be shown when screen is focused or matches change
|
// Check if tutorial should be shown when screen is focused or matches change
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -78,52 +150,77 @@ export default function MatchesScreen() {
|
|||||||
onPress={() => router.push(`/chat/${item.id}`)}
|
onPress={() => router.push(`/chat/${item.id}`)}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<GlassCard style={styles.matchCard}>
|
<View style={styles.matchCardPremium}>
|
||||||
<View style={styles.matchRow}>
|
{/* Subtle gradient background */}
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={isYesYes ? gradients.primary as [string, string] : [colors.glass.background, colors.glass.background]}
|
colors={['rgba(22, 33, 62, 0.6)', 'rgba(13, 13, 26, 0.8)']}
|
||||||
style={styles.iconContainer}
|
style={StyleSheet.absoluteFill}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
>
|
/>
|
||||||
|
{/* Top silk highlight */}
|
||||||
|
<LinearGradient
|
||||||
|
colors={[`${ACCENT_RGBA}0.06)`, 'transparent']}
|
||||||
|
style={styles.cardSilkHighlight}
|
||||||
|
start={{ x: 0.5, y: 0 }}
|
||||||
|
end={{ x: 0.5, y: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.matchRow}>
|
||||||
|
{/* Premium icon container */}
|
||||||
|
<View style={[
|
||||||
|
styles.iconContainerPremium,
|
||||||
|
isYesYes && styles.iconContainerYesYes
|
||||||
|
]}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={isYesYes ? "heart" : "heart-half"}
|
name={isYesYes ? "heart" : "heart-half"}
|
||||||
size={22}
|
size={20}
|
||||||
color={isYesYes ? colors.text : colors.primary}
|
color={isYesYes ? ACCENT : ROSE}
|
||||||
/>
|
/>
|
||||||
</LinearGradient>
|
</View>
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.questionText} numberOfLines={2}>
|
<Text style={styles.questionTextPremium} numberOfLines={2}>
|
||||||
{userText}
|
{userText}
|
||||||
</Text>
|
</Text>
|
||||||
{item.question.partner_text && (
|
{item.question.partner_text && (
|
||||||
<Text style={styles.partnerText} numberOfLines={1}>
|
<Text style={styles.partnerTextPremium} numberOfLines={1}>
|
||||||
Partner: {partnerText}
|
Partner: {partnerText}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<View style={styles.metaRow}>
|
<View style={styles.metaRow}>
|
||||||
<View style={[styles.tag, isYesYes && styles.tagHighlight]}>
|
<View style={[
|
||||||
<Text style={[styles.tagText, isYesYes && styles.tagTextHighlight]}>
|
styles.tagPremium,
|
||||||
|
isYesYes && styles.tagPremiumHighlight
|
||||||
|
]}>
|
||||||
|
<Text style={[
|
||||||
|
styles.tagTextPremium,
|
||||||
|
isYesYes && styles.tagTextPremiumHighlight
|
||||||
|
]}>
|
||||||
{isYesYes ? "YES + YES" : "YES + MAYBE"}
|
{isYesYes ? "YES + YES" : "YES + MAYBE"}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.date}>
|
<Text style={styles.datePremium}>
|
||||||
{new Date(item.created_at).toLocaleDateString()}
|
{new Date(item.created_at).toLocaleDateString()}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.rightSection}>
|
<View style={styles.rightSection}>
|
||||||
{item.unreadCount > 0 && (
|
{item.unreadCount > 0 && (
|
||||||
<View style={styles.unreadBadge}>
|
<View style={styles.unreadBadgePremium}>
|
||||||
<Ionicons name="chatbubble" size={12} color={colors.text} />
|
<Ionicons name="chatbubble" size={10} color={colors.text} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View style={styles.chevronContainer}>
|
<View style={styles.chevronContainerPremium}>
|
||||||
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
|
<Ionicons name="chevron-forward" size={16} color={`${ACCENT_RGBA}0.6)`} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</GlassCard>
|
|
||||||
|
{/* Premium border */}
|
||||||
|
<View style={styles.cardPremiumBorder} pointerEvents="none" />
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
@@ -141,21 +238,48 @@ export default function MatchesScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientBackground>
|
<GradientBackground>
|
||||||
|
{/* Ambient Orbs - Premium gold/rose */}
|
||||||
|
<Animated.View style={[styles.ambientOrb, styles.orbTopRight, orbStyle1]} pointerEvents="none">
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.premium.goldGlow, 'transparent']}
|
||||||
|
style={styles.orbGradient}
|
||||||
|
start={{ x: 0.5, y: 0.5 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
<Animated.View style={[styles.ambientOrb, styles.orbBottomLeft, orbStyle2]} pointerEvents="none">
|
||||||
|
<LinearGradient
|
||||||
|
colors={[`${ROSE_RGBA}0.2)`, 'transparent']}
|
||||||
|
style={styles.orbGradient}
|
||||||
|
start={{ x: 0.5, y: 0.5 }}
|
||||||
|
end={{ x: 0, y: 0 }}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
{/* Premium Header */}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInDown.duration(400)}
|
entering={FadeIn.duration(400)}
|
||||||
style={[styles.header, isWideScreen && styles.headerWide]}
|
style={[styles.header, isWideScreen && styles.headerWide]}
|
||||||
>
|
>
|
||||||
<View style={styles.headerTop}>
|
<View style={styles.headerContent}>
|
||||||
<Text style={styles.title}>Matches</Text>
|
{/* Premium label */}
|
||||||
<View style={styles.countBadge}>
|
<Text style={styles.headerLabel}>DISCOVER</Text>
|
||||||
<Ionicons name="heart" size={14} color={colors.primary} />
|
<Text style={styles.headerTitle}>Matches</Text>
|
||||||
<Text style={styles.countText}>{matches.length}</Text>
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.headerSeparator}>
|
||||||
|
<View style={styles.headerSeparatorLine} />
|
||||||
|
<View style={styles.headerSeparatorDiamond} />
|
||||||
|
<View style={styles.headerSeparatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Count badge */}
|
||||||
|
<View style={styles.countBadgePremium}>
|
||||||
|
<Ionicons name="heart" size={12} color={ACCENT} />
|
||||||
|
<Text style={styles.countTextPremium}>{matches.length} {matches.length === 1 ? 'MATCH' : 'MATCHES'}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.subtitle}>
|
|
||||||
Discover what you both enjoy together
|
|
||||||
</Text>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
@@ -174,38 +298,63 @@ export default function MatchesScreen() {
|
|||||||
}
|
}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInDown.delay(200).duration(400)}
|
entering={FadeInUp.duration(600).springify()}
|
||||||
style={styles.emptyContainer}
|
style={styles.emptyContent}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
{/* Premium icon */}
|
||||||
colors={gradients.primary as [string, string]}
|
<View style={styles.emptyIconContainerPremium}>
|
||||||
style={styles.emptyIconGradient}
|
<Ionicons name="heart-outline" size={36} color={ROSE} />
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
>
|
|
||||||
<View style={styles.emptyIconInner}>
|
|
||||||
<Ionicons name="heart-outline" size={40} color={colors.primary} />
|
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
|
||||||
<Text style={styles.emptyTitle}>No matches yet</Text>
|
{/* Title section */}
|
||||||
<Text style={styles.emptyText}>
|
<Text style={styles.emptyLabel}>BEGIN</Text>
|
||||||
Keep swiping to find things you both enjoy!
|
<Text style={styles.emptyTitlePremium}>No Matches Yet</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.emptySeparator}>
|
||||||
|
<View style={styles.emptySeparatorLine} />
|
||||||
|
<View style={styles.emptySeparatorDiamond} />
|
||||||
|
<View style={styles.emptySeparatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Status badge */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.delay(300).duration(400)}
|
||||||
|
style={styles.emptyBadge}
|
||||||
|
>
|
||||||
|
<Text style={styles.emptyBadgeText}>START EXPLORING</Text>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Text style={styles.emptyDescription}>
|
||||||
|
Answer questions together to discover what you both enjoy. Matches appear when you agree!
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.emptyButton}
|
{/* Feature hints */}
|
||||||
|
<View style={styles.emptyFeatures}>
|
||||||
|
<View style={styles.emptyFeatureItem}>
|
||||||
|
<Ionicons name="heart" size={16} color={ROSE} />
|
||||||
|
<Text style={styles.emptyFeatureText}>Swipe right for yes</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.emptyFeatureItem}>
|
||||||
|
<Ionicons name="sparkles" size={16} color={ROSE} />
|
||||||
|
<Text style={styles.emptyFeatureText}>Match when you both agree</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.emptyFeatureItem}>
|
||||||
|
<Ionicons name="chatbubbles-outline" size={16} color={ROSE} />
|
||||||
|
<Text style={styles.emptyFeatureText}>Chat about your matches</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom teaser */}
|
||||||
|
<Text style={styles.emptyTeaser}>Your first match is just a swipe away</Text>
|
||||||
|
|
||||||
|
<GlassButton
|
||||||
onPress={() => router.push("/(app)/swipe")}
|
onPress={() => router.push("/(app)/swipe")}
|
||||||
activeOpacity={0.8}
|
style={{ marginTop: spacing.lg }}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
Start Swiping
|
||||||
colors={gradients.primary as [string, string]}
|
</GlassButton>
|
||||||
style={styles.emptyButtonGradient}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
>
|
|
||||||
<Ionicons name="flame" size={18} color={colors.text} />
|
|
||||||
<Text style={styles.emptyButtonText}>Start Swiping</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -228,47 +377,93 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
// Ambient orbs
|
||||||
|
ambientOrb: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
borderRadius: 150,
|
||||||
|
},
|
||||||
|
orbTopRight: {
|
||||||
|
top: 60,
|
||||||
|
right: -40,
|
||||||
|
},
|
||||||
|
orbBottomLeft: {
|
||||||
|
bottom: 180,
|
||||||
|
left: -40,
|
||||||
|
},
|
||||||
|
orbGradient: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 150,
|
||||||
|
},
|
||||||
|
// Premium Header
|
||||||
header: {
|
header: {
|
||||||
paddingTop: 60,
|
paddingTop: 60,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingBottom: spacing.lg,
|
paddingBottom: spacing.md,
|
||||||
},
|
},
|
||||||
headerWide: {
|
headerWide: {
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: MAX_CONTENT_WIDTH,
|
maxWidth: MAX_CONTENT_WIDTH,
|
||||||
},
|
},
|
||||||
headerTop: {
|
headerContent: {
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
},
|
||||||
title: {
|
headerLabel: {
|
||||||
...typography.title1,
|
...typography.caption2,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: ACCENT,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
...typography.largeTitle,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
countBadge: {
|
headerSeparator: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.primaryLight,
|
justifyContent: 'center',
|
||||||
paddingHorizontal: spacing.sm,
|
marginVertical: spacing.md,
|
||||||
paddingVertical: spacing.xs,
|
width: 100,
|
||||||
|
},
|
||||||
|
headerSeparatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: `${ACCENT_RGBA}0.3)`,
|
||||||
|
},
|
||||||
|
headerSeparatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: ACCENT,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.sm,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
countBadgePremium: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: `${ACCENT_RGBA}0.1)`,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
borderRadius: radius.full,
|
borderRadius: radius.full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: `${ACCENT_RGBA}0.2)`,
|
||||||
gap: spacing.xs,
|
gap: spacing.xs,
|
||||||
},
|
},
|
||||||
countText: {
|
countTextPremium: {
|
||||||
...typography.subhead,
|
...typography.caption2,
|
||||||
color: colors.text,
|
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: ACCENT,
|
||||||
},
|
},
|
||||||
subtitle: {
|
// List
|
||||||
...typography.subhead,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginTop: spacing.xs,
|
|
||||||
},
|
|
||||||
list: {
|
list: {
|
||||||
padding: spacing.lg,
|
padding: spacing.lg,
|
||||||
paddingTop: 0,
|
paddingTop: spacing.sm,
|
||||||
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
||||||
},
|
},
|
||||||
listWide: {
|
listWide: {
|
||||||
@@ -276,32 +471,60 @@ const styles = StyleSheet.create({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: MAX_CONTENT_WIDTH,
|
maxWidth: MAX_CONTENT_WIDTH,
|
||||||
},
|
},
|
||||||
matchCard: {
|
// Premium Match Card
|
||||||
|
matchCardPremium: {
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
|
borderRadius: radius.lg,
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: spacing.md,
|
||||||
|
},
|
||||||
|
cardSilkHighlight: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 60,
|
||||||
|
},
|
||||||
|
cardPremiumBorder: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
borderRadius: radius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: `${ACCENT_RGBA}0.15)`,
|
||||||
},
|
},
|
||||||
matchRow: {
|
matchRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
iconContainer: {
|
iconContainerPremium: {
|
||||||
width: 48,
|
width: 44,
|
||||||
height: 48,
|
height: 44,
|
||||||
borderRadius: 24,
|
borderRadius: 22,
|
||||||
|
backgroundColor: `${ROSE_RGBA}0.1)`,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: `${ROSE_RGBA}0.2)`,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginRight: spacing.md,
|
marginRight: spacing.md,
|
||||||
},
|
},
|
||||||
|
iconContainerYesYes: {
|
||||||
|
backgroundColor: `${ACCENT_RGBA}0.1)`,
|
||||||
|
borderColor: `${ACCENT_RGBA}0.2)`,
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
questionText: {
|
questionTextPremium: {
|
||||||
...typography.body,
|
...typography.subhead,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
marginBottom: spacing.xs,
|
marginBottom: spacing.xs,
|
||||||
lineHeight: 22,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
partnerText: {
|
partnerTextPremium: {
|
||||||
...typography.caption1,
|
...typography.caption1,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
fontStyle: "italic",
|
fontStyle: "italic",
|
||||||
@@ -312,27 +535,31 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
},
|
},
|
||||||
tag: {
|
tagPremium: {
|
||||||
backgroundColor: colors.glass.background,
|
backgroundColor: `${ROSE_RGBA}0.1)`,
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
paddingVertical: 4,
|
paddingVertical: 3,
|
||||||
borderRadius: radius.sm,
|
borderRadius: radius.sm,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: `${ROSE_RGBA}0.15)`,
|
||||||
},
|
},
|
||||||
tagHighlight: {
|
tagPremiumHighlight: {
|
||||||
backgroundColor: colors.primaryLight,
|
backgroundColor: `${ACCENT_RGBA}0.1)`,
|
||||||
|
borderColor: `${ACCENT_RGBA}0.2)`,
|
||||||
},
|
},
|
||||||
tagText: {
|
tagTextPremium: {
|
||||||
...typography.caption2,
|
...typography.caption2,
|
||||||
color: colors.textSecondary,
|
color: ROSE,
|
||||||
fontWeight: "700",
|
fontWeight: "600",
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 1,
|
||||||
},
|
},
|
||||||
tagTextHighlight: {
|
tagTextPremiumHighlight: {
|
||||||
color: colors.primary,
|
color: ACCENT,
|
||||||
},
|
},
|
||||||
date: {
|
datePremium: {
|
||||||
...typography.caption2,
|
...typography.caption2,
|
||||||
color: colors.textTertiary,
|
color: colors.textTertiary,
|
||||||
|
fontStyle: 'italic',
|
||||||
},
|
},
|
||||||
rightSection: {
|
rightSection: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -340,70 +567,115 @@ const styles = StyleSheet.create({
|
|||||||
gap: spacing.sm,
|
gap: spacing.sm,
|
||||||
marginLeft: spacing.sm,
|
marginLeft: spacing.sm,
|
||||||
},
|
},
|
||||||
unreadBadge: {
|
unreadBadgePremium: {
|
||||||
width: 24,
|
width: 22,
|
||||||
height: 24,
|
height: 22,
|
||||||
borderRadius: 12,
|
borderRadius: 11,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: ACCENT,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
chevronContainer: {
|
chevronContainerPremium: {
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: colors.glass.background,
|
backgroundColor: `${ACCENT_RGBA}0.08)`,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: `${ACCENT_RGBA}0.15)`,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
emptyContainer: {
|
// Premium Empty State
|
||||||
alignItems: "center",
|
emptyContent: {
|
||||||
paddingVertical: spacing.xxl,
|
width: '100%',
|
||||||
paddingHorizontal: spacing.lg,
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingVertical: spacing.xl,
|
||||||
},
|
},
|
||||||
emptyIconGradient: {
|
emptyIconContainerPremium: {
|
||||||
width: 100,
|
width: 72,
|
||||||
height: 100,
|
height: 72,
|
||||||
borderRadius: 50,
|
borderRadius: 36,
|
||||||
|
backgroundColor: `${ROSE_RGBA}0.1)`,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: `${ROSE_RGBA}0.2)`,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: spacing.lg,
|
|
||||||
...shadows.lg,
|
|
||||||
},
|
|
||||||
emptyIconInner: {
|
|
||||||
width: 88,
|
|
||||||
height: 88,
|
|
||||||
borderRadius: 44,
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
emptyTitle: {
|
|
||||||
...typography.title2,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: spacing.sm,
|
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
...typography.body,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textAlign: "center",
|
|
||||||
marginBottom: spacing.xl,
|
marginBottom: spacing.xl,
|
||||||
},
|
},
|
||||||
emptyButton: {
|
emptyLabel: {
|
||||||
borderRadius: radius.full,
|
...typography.caption1,
|
||||||
overflow: 'hidden',
|
fontWeight: '600',
|
||||||
...shadows.md,
|
letterSpacing: 3,
|
||||||
|
color: ROSE,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xs,
|
||||||
},
|
},
|
||||||
emptyButtonGradient: {
|
emptyTitlePremium: {
|
||||||
|
...typography.largeTitle,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
emptySeparator: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.lg,
|
justifyContent: 'center',
|
||||||
paddingVertical: spacing.md,
|
marginVertical: spacing.lg,
|
||||||
gap: spacing.sm,
|
width: 140,
|
||||||
},
|
},
|
||||||
emptyButtonText: {
|
emptySeparatorLine: {
|
||||||
...typography.body,
|
flex: 1,
|
||||||
color: colors.text,
|
height: 1,
|
||||||
|
backgroundColor: `${ROSE_RGBA}0.3)`,
|
||||||
|
},
|
||||||
|
emptySeparatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: ROSE,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
emptyBadge: {
|
||||||
|
backgroundColor: `${ROSE_RGBA}0.1)`,
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: `${ROSE_RGBA}0.2)`,
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
emptyBadgeText: {
|
||||||
|
...typography.caption2,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: ROSE,
|
||||||
|
},
|
||||||
|
emptyDescription: {
|
||||||
|
...typography.body,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
},
|
||||||
|
emptyFeatures: {
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
emptyFeatureItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
emptyFeatureText: {
|
||||||
|
...typography.subhead,
|
||||||
|
color: colors.text,
|
||||||
|
marginLeft: spacing.sm,
|
||||||
|
},
|
||||||
|
emptyTeaser: {
|
||||||
|
...typography.footnote,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
color: colors.textTertiary,
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -605,7 +605,7 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingTop: Platform.OS === 'ios' ? 60 : 40,
|
paddingTop: 60,
|
||||||
paddingBottom: spacing.md,
|
paddingBottom: spacing.md,
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,300 +0,0 @@
|
|||||||
import { View, Text, StyleSheet, ScrollView, RefreshControl, Platform, Alert } from "react-native";
|
|
||||||
import { usePacksStore, useAuthStore, useSubscriptionStore } from "../../src/store";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
|
||||||
import { useEffect, useMemo, useState, useCallback } from "react";
|
|
||||||
import Animated, { FadeInDown, FadeIn, Layout } from "react-native-reanimated";
|
|
||||||
import { GradientBackground } from "../../src/components/ui";
|
|
||||||
import { Paywall } from "../../src/components/Paywall";
|
|
||||||
import { PackTeaser } from "../../src/components/PackTeaser";
|
|
||||||
import { CategoryTabs, FilterType } from "../../src/components/CategoryTabs";
|
|
||||||
import { BoutiquePackCard } from "../../src/components/BoutiquePackCard";
|
|
||||||
import { colors, spacing, typography } from "../../src/theme";
|
|
||||||
import type { QuestionPack } from "../../src/types";
|
|
||||||
|
|
||||||
export default function PacksScreen() {
|
|
||||||
const { category } = useLocalSearchParams<{ category?: string }>();
|
|
||||||
const { packs, categories, enabledPackIds, togglePack, isLoading, fetchPacks } = usePacksStore();
|
|
||||||
const { user, partner } = useAuthStore();
|
|
||||||
const { subscription } = useSubscriptionStore();
|
|
||||||
const [selectedFilter, setSelectedFilter] = useState<FilterType>('all');
|
|
||||||
const [showPaywall, setShowPaywall] = useState(false);
|
|
||||||
const [showTeaser, setShowTeaser] = useState(false);
|
|
||||||
const [teaserPack, setTeaserPack] = useState<QuestionPack | null>(null);
|
|
||||||
|
|
||||||
// Check if user or partner has premium access
|
|
||||||
const hasPremiumAccess = useMemo(() => {
|
|
||||||
return user?.is_premium || partner?.is_premium || subscription.isProUser;
|
|
||||||
}, [user?.is_premium, partner?.is_premium, subscription.isProUser]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPacks();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Apply category filter from URL param
|
|
||||||
useEffect(() => {
|
|
||||||
if (category) {
|
|
||||||
setSelectedFilter(category);
|
|
||||||
}
|
|
||||||
}, [category]);
|
|
||||||
|
|
||||||
// Filter packs by selected filter (all, enabled, or category id)
|
|
||||||
const filteredPacks = useMemo(() => {
|
|
||||||
if (selectedFilter === 'all') return packs;
|
|
||||||
if (selectedFilter === 'enabled') return packs.filter(p => enabledPackIds.includes(p.id));
|
|
||||||
// Otherwise it's a category id
|
|
||||||
return packs.filter(p => p.category_id === selectedFilter);
|
|
||||||
}, [packs, selectedFilter, enabledPackIds]);
|
|
||||||
|
|
||||||
// Split packs into two columns for masonry layout
|
|
||||||
const { leftColumn, rightColumn } = useMemo(() => {
|
|
||||||
const left: { pack: QuestionPack; index: number }[] = [];
|
|
||||||
const right: { pack: QuestionPack; index: number }[] = [];
|
|
||||||
let leftHeight = 0;
|
|
||||||
let rightHeight = 0;
|
|
||||||
|
|
||||||
filteredPacks.forEach((pack, index) => {
|
|
||||||
// Estimate card height based on description length
|
|
||||||
const baseHeight = 180;
|
|
||||||
const descLength = pack.description?.length || 0;
|
|
||||||
const extraHeight = Math.min(Math.ceil(descLength / 40) * 10, 40);
|
|
||||||
const cardHeight = baseHeight + extraHeight;
|
|
||||||
|
|
||||||
if (leftHeight <= rightHeight) {
|
|
||||||
left.push({ pack, index });
|
|
||||||
leftHeight += cardHeight;
|
|
||||||
} else {
|
|
||||||
right.push({ pack, index });
|
|
||||||
rightHeight += cardHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { leftColumn: left, rightColumn: right };
|
|
||||||
}, [filteredPacks]);
|
|
||||||
|
|
||||||
// Handle toggling a pack - show teaser if premium pack without access
|
|
||||||
const handleTogglePack = useCallback(async (pack: QuestionPack) => {
|
|
||||||
if (pack.is_premium && !hasPremiumAccess) {
|
|
||||||
setTeaserPack(pack);
|
|
||||||
setShowTeaser(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await togglePack(pack.id);
|
|
||||||
if (!result.success && result.reason === "no_couple") {
|
|
||||||
Alert.alert(
|
|
||||||
"Pair with Partner",
|
|
||||||
"You need to pair with your partner before you can customise question packs.",
|
|
||||||
[
|
|
||||||
{ text: "Later", style: "cancel" },
|
|
||||||
{ text: "Pair Now", onPress: () => router.push("/pairing") }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [hasPremiumAccess, togglePack]);
|
|
||||||
|
|
||||||
// Handle pack press - navigate or show teaser
|
|
||||||
const handlePackPress = useCallback((pack: QuestionPack) => {
|
|
||||||
const isPremiumLocked = pack.is_premium && !hasPremiumAccess;
|
|
||||||
if (isPremiumLocked) {
|
|
||||||
setTeaserPack(pack);
|
|
||||||
setShowTeaser(true);
|
|
||||||
} else {
|
|
||||||
router.push(`/pack/${pack.id}`);
|
|
||||||
}
|
|
||||||
}, [hasPremiumAccess]);
|
|
||||||
|
|
||||||
// Handle teaser unlock button - show paywall
|
|
||||||
const handleTeaserUnlock = useCallback(() => {
|
|
||||||
setShowTeaser(false);
|
|
||||||
setShowPaywall(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle teaser close
|
|
||||||
const handleTeaserClose = useCallback(() => {
|
|
||||||
setShowTeaser(false);
|
|
||||||
setTeaserPack(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const enabledPacks = packs.filter(p => enabledPackIds.includes(p.id));
|
|
||||||
const enabledPackCount = enabledPacks.length;
|
|
||||||
const totalQuestions = enabledPacks.reduce((sum, p) => sum + (p.questions?.[0]?.count || 0), 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GradientBackground>
|
|
||||||
<View style={styles.container}>
|
|
||||||
{/* Header */}
|
|
||||||
<Animated.View
|
|
||||||
entering={FadeInDown.duration(400)}
|
|
||||||
style={styles.header}
|
|
||||||
>
|
|
||||||
<Text style={styles.title}>Question Packs</Text>
|
|
||||||
<Text style={styles.subtitle}>
|
|
||||||
{totalQuestions} {totalQuestions === 1 ? 'question' : 'questions'} from {enabledPackCount} {enabledPackCount === 1 ? 'pack' : 'packs'} enabled
|
|
||||||
</Text>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
{/* Category Tabs */}
|
|
||||||
<Animated.View entering={FadeInDown.delay(100).duration(400)}>
|
|
||||||
<CategoryTabs
|
|
||||||
categories={categories}
|
|
||||||
selectedFilter={selectedFilter}
|
|
||||||
onSelectFilter={setSelectedFilter}
|
|
||||||
enabledCount={enabledPackIds.length}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
{/* Masonry Grid */}
|
|
||||||
<ScrollView
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isLoading}
|
|
||||||
onRefresh={fetchPacks}
|
|
||||||
tintColor={colors.primary}
|
|
||||||
colors={[colors.primary]}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
>
|
|
||||||
{filteredPacks.length === 0 ? (
|
|
||||||
<Animated.View
|
|
||||||
entering={FadeInDown.delay(200).duration(400)}
|
|
||||||
style={styles.emptyContainer}
|
|
||||||
>
|
|
||||||
<View style={styles.emptyIconContainer}>
|
|
||||||
<Ionicons name="layers-outline" size={48} color={colors.textTertiary} />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.emptyTitle}>No packs available</Text>
|
|
||||||
<Text style={styles.emptyText}>
|
|
||||||
{selectedFilter === 'enabled'
|
|
||||||
? "No packs enabled yet. Enable some packs to see them here!"
|
|
||||||
: selectedFilter !== 'all'
|
|
||||||
? "No packs in this category. Try selecting a different one."
|
|
||||||
: "Check back later for new question packs!"}
|
|
||||||
</Text>
|
|
||||||
</Animated.View>
|
|
||||||
) : (
|
|
||||||
<View style={styles.masonryContainer}>
|
|
||||||
{/* Left Column */}
|
|
||||||
<View style={styles.column}>
|
|
||||||
{leftColumn.map(({ pack, index }, columnIndex) => (
|
|
||||||
<Animated.View
|
|
||||||
key={pack.id}
|
|
||||||
entering={FadeInDown.delay(columnIndex * 80).duration(400)}
|
|
||||||
layout={Layout.springify()}
|
|
||||||
>
|
|
||||||
<BoutiquePackCard
|
|
||||||
pack={pack}
|
|
||||||
index={index}
|
|
||||||
isEnabled={enabledPackIds.includes(pack.id)}
|
|
||||||
isPremiumLocked={pack.is_premium && !hasPremiumAccess}
|
|
||||||
onPress={() => handlePackPress(pack)}
|
|
||||||
onToggle={() => handleTogglePack(pack)}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Right Column */}
|
|
||||||
<View style={styles.column}>
|
|
||||||
{rightColumn.map(({ pack, index }, columnIndex) => (
|
|
||||||
<Animated.View
|
|
||||||
key={pack.id}
|
|
||||||
entering={FadeInDown.delay(columnIndex * 80 + 40).duration(400)}
|
|
||||||
layout={Layout.springify()}
|
|
||||||
>
|
|
||||||
<BoutiquePackCard
|
|
||||||
pack={pack}
|
|
||||||
index={index}
|
|
||||||
isEnabled={enabledPackIds.includes(pack.id)}
|
|
||||||
isPremiumLocked={pack.is_premium && !hasPremiumAccess}
|
|
||||||
onPress={() => handlePackPress(pack)}
|
|
||||||
onToggle={() => handleTogglePack(pack)}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
{/* Modals */}
|
|
||||||
{teaserPack && (
|
|
||||||
<PackTeaser
|
|
||||||
visible={showTeaser}
|
|
||||||
packId={teaserPack.id}
|
|
||||||
packName={teaserPack.name}
|
|
||||||
packIcon={teaserPack.icon}
|
|
||||||
onClose={handleTeaserClose}
|
|
||||||
onUnlock={handleTeaserUnlock}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Paywall
|
|
||||||
visible={showPaywall}
|
|
||||||
onClose={() => setShowPaywall(false)}
|
|
||||||
onSuccess={() => {
|
|
||||||
// Refresh packs after successful purchase
|
|
||||||
fetchPacks();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</GradientBackground>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
paddingTop: 60,
|
|
||||||
paddingHorizontal: spacing.lg,
|
|
||||||
paddingBottom: spacing.sm,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
...typography.title1,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
...typography.subhead,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginTop: spacing.xs,
|
|
||||||
},
|
|
||||||
scrollContent: {
|
|
||||||
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
|
||||||
},
|
|
||||||
masonryContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
paddingHorizontal: spacing.md,
|
|
||||||
gap: spacing.md,
|
|
||||||
},
|
|
||||||
column: {
|
|
||||||
flex: 1,
|
|
||||||
gap: spacing.md,
|
|
||||||
},
|
|
||||||
emptyContainer: {
|
|
||||||
alignItems: "center",
|
|
||||||
paddingVertical: spacing.xxl,
|
|
||||||
paddingHorizontal: spacing.lg,
|
|
||||||
},
|
|
||||||
emptyIconContainer: {
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
borderRadius: 40,
|
|
||||||
backgroundColor: colors.glass.background,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: spacing.md,
|
|
||||||
},
|
|
||||||
emptyTitle: {
|
|
||||||
...typography.title3,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: spacing.xs,
|
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
...typography.body,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Stack } from "expo-router";
|
||||||
|
import { colors } from "../../../src/theme";
|
||||||
|
|
||||||
|
export default function PacksLayout() {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
contentStyle: { backgroundColor: colors.background },
|
||||||
|
animation: "fade",
|
||||||
|
animationDuration: 150,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
import { View, Text, StyleSheet, Pressable, Platform } from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||||
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
|
import { GradientBackground } from "../../../src/components/ui";
|
||||||
|
import { usePacksStore } from "../../../src/store";
|
||||||
|
import { colors, spacing, radius, typography, shadows } from "../../../src/theme";
|
||||||
|
|
||||||
|
interface PackTypeCard {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: keyof typeof Ionicons.glyphMap;
|
||||||
|
color: string;
|
||||||
|
colorRgba: string;
|
||||||
|
gradient: [string, string];
|
||||||
|
comingSoon: boolean;
|
||||||
|
route?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PACK_TYPES: PackTypeCard[] = [
|
||||||
|
{
|
||||||
|
id: "questions",
|
||||||
|
title: "Question Packs",
|
||||||
|
description: "Explore intimate questions together and discover what you both want",
|
||||||
|
icon: "layers",
|
||||||
|
color: colors.primary,
|
||||||
|
colorRgba: "rgba(233, 69, 96, ",
|
||||||
|
gradient: ["rgba(233, 69, 96, 0.25)", "rgba(155, 89, 182, 0.25)"],
|
||||||
|
comingSoon: false,
|
||||||
|
route: "/(app)/packs/questions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dares",
|
||||||
|
title: "Dare Packs",
|
||||||
|
description: "Spice things up with exciting dares for adventurous couples",
|
||||||
|
icon: "flash",
|
||||||
|
color: colors.premium.gold,
|
||||||
|
colorRgba: "rgba(212, 175, 55, ",
|
||||||
|
gradient: ["rgba(212, 175, 55, 0.25)", "rgba(184, 134, 11, 0.25)"],
|
||||||
|
comingSoon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "quiz",
|
||||||
|
title: "Quiz Packs",
|
||||||
|
description: "Test how well you know each other with fun couple quizzes",
|
||||||
|
icon: "help-circle",
|
||||||
|
color: colors.premium.rose,
|
||||||
|
colorRgba: "rgba(232, 164, 174, ",
|
||||||
|
gradient: ["rgba(232, 164, 174, 0.25)", "rgba(212, 145, 155, 0.25)"],
|
||||||
|
comingSoon: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PackTypeSelection() {
|
||||||
|
const { fetchPacks } = usePacksStore();
|
||||||
|
|
||||||
|
// Prefetch packs data so it's ready when navigating to questions
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPacks();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePackTypePress = (packType: PackTypeCard) => {
|
||||||
|
if (!packType.comingSoon && packType.route) {
|
||||||
|
router.push(packType.route as any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GradientBackground>
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.duration(600).springify()}
|
||||||
|
style={styles.header}
|
||||||
|
>
|
||||||
|
<Text style={styles.label}>EXPLORE</Text>
|
||||||
|
<Text style={styles.title}>Content Packs</Text>
|
||||||
|
|
||||||
|
{/* Decorative Separator */}
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
Choose a pack type to explore
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Pack Type Cards */}
|
||||||
|
<View style={styles.cardsContainer}>
|
||||||
|
{PACK_TYPES.map((packType, index) => (
|
||||||
|
<Animated.View
|
||||||
|
key={packType.id}
|
||||||
|
entering={FadeInDown.delay(100 + index * 100).duration(500).springify()}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => handlePackTypePress(packType)}
|
||||||
|
style={({ pressed }) => [
|
||||||
|
styles.card,
|
||||||
|
pressed && !packType.comingSoon && styles.cardPressed,
|
||||||
|
packType.comingSoon && styles.cardDisabled,
|
||||||
|
]}
|
||||||
|
disabled={packType.comingSoon}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={packType.gradient as [string, string]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
style={styles.cardGradient}
|
||||||
|
>
|
||||||
|
{/* Icon Circle */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.iconCircle,
|
||||||
|
{
|
||||||
|
backgroundColor: `${packType.colorRgba}0.15)`,
|
||||||
|
borderColor: `${packType.colorRgba}0.3)`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={packType.icon}
|
||||||
|
size={32}
|
||||||
|
color={packType.comingSoon ? colors.textTertiary : packType.color}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<View style={styles.cardTitleRow}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.cardTitle,
|
||||||
|
packType.comingSoon && styles.cardTitleDisabled,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{packType.title}
|
||||||
|
</Text>
|
||||||
|
{packType.comingSoon && (
|
||||||
|
<View style={styles.comingSoonBadge}>
|
||||||
|
<Text style={styles.comingSoonText}>Coming Soon</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.cardDescription,
|
||||||
|
packType.comingSoon && styles.cardDescriptionDisabled,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{packType.description}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Arrow */}
|
||||||
|
{!packType.comingSoon && (
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-forward"
|
||||||
|
size={24}
|
||||||
|
color={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</LinearGradient>
|
||||||
|
</Pressable>
|
||||||
|
</Animated.View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</GradientBackground>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
paddingTop: 60,
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingBottom: spacing.lg,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontWeight: "600",
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: colors.primary,
|
||||||
|
textAlign: "center",
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
...typography.title1,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
separatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: "rgba(233, 69, 96, 0.3)",
|
||||||
|
},
|
||||||
|
separatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
transform: [{ rotate: "45deg" }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
...typography.body,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
cardsContainer: {
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
gap: spacing.md,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
borderRadius: radius.lg,
|
||||||
|
overflow: "hidden",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.glass.border,
|
||||||
|
...shadows.md,
|
||||||
|
},
|
||||||
|
cardPressed: {
|
||||||
|
opacity: 0.9,
|
||||||
|
transform: [{ scale: 0.98 }],
|
||||||
|
},
|
||||||
|
cardDisabled: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
cardGradient: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: spacing.lg,
|
||||||
|
gap: spacing.md,
|
||||||
|
},
|
||||||
|
iconCircle: {
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
cardContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
cardTitleRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: spacing.sm,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
cardTitle: {
|
||||||
|
...typography.title3,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
cardTitleDisabled: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
cardDescription: {
|
||||||
|
...typography.subhead,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
cardDescriptionDisabled: {
|
||||||
|
color: colors.textTertiary,
|
||||||
|
},
|
||||||
|
comingSoonBadge: {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
},
|
||||||
|
comingSoonText: {
|
||||||
|
...typography.caption2,
|
||||||
|
color: colors.textTertiary,
|
||||||
|
fontWeight: "500",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,711 @@
|
|||||||
|
import { View, Text, StyleSheet, RefreshControl, Platform, Alert, Pressable } from "react-native";
|
||||||
|
import { usePacksStore, useAuthStore, useSubscriptionStore } from "../../../src/store";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { useMemo, useState, useCallback } from "react";
|
||||||
|
import Animated, {
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withTiming,
|
||||||
|
useAnimatedScrollHandler,
|
||||||
|
interpolate,
|
||||||
|
Extrapolation,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
import { BlurView } from "expo-blur";
|
||||||
|
import { GradientBackground } from "../../../src/components/ui";
|
||||||
|
import { Paywall } from "../../../src/components/Paywall";
|
||||||
|
import { PackTeaser } from "../../../src/components/PackTeaser";
|
||||||
|
import { colors, spacing, radius, typography } from "../../../src/theme";
|
||||||
|
import type { QuestionPack, Category } from "../../../src/types";
|
||||||
|
|
||||||
|
const HEADER_MAX_HEIGHT = 180;
|
||||||
|
const HEADER_MIN_HEIGHT = 100;
|
||||||
|
const HEADER_SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT;
|
||||||
|
|
||||||
|
interface PackRowProps {
|
||||||
|
pack: QuestionPack;
|
||||||
|
isEnabled: boolean;
|
||||||
|
isPremiumLocked: boolean;
|
||||||
|
isExpanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
onPress: () => void;
|
||||||
|
onInfoPress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PackRow({ pack, isEnabled, isPremiumLocked, isExpanded, onToggle, onPress, onInfoPress }: PackRowProps) {
|
||||||
|
const questionCount = pack.questions?.[0]?.count || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable onPress={onPress} style={styles.packRow}>
|
||||||
|
<View style={styles.packRowMain}>
|
||||||
|
<View style={[styles.packIcon, isEnabled && styles.packIconEnabled]}>
|
||||||
|
<Text style={styles.packIconText}>{pack.icon || "📦"}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.packContent}>
|
||||||
|
<View style={styles.packTitleRow}>
|
||||||
|
<Text style={styles.packName} numberOfLines={1}>{pack.name}</Text>
|
||||||
|
{isPremiumLocked && (
|
||||||
|
<Ionicons name="lock-closed" size={14} color={colors.premium.gold} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.packMeta}>
|
||||||
|
{questionCount} {questionCount === 1 ? "question" : "questions"}
|
||||||
|
{isEnabled && <Text style={styles.packMetaEnabled}> · Enabled</Text>}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Ionicons
|
||||||
|
name={isExpanded ? "chevron-up" : "chevron-down"}
|
||||||
|
size={20}
|
||||||
|
color={colors.textTertiary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.duration(200)}
|
||||||
|
exiting={FadeOut.duration(150)}
|
||||||
|
style={styles.packExpanded}
|
||||||
|
>
|
||||||
|
{pack.description && (
|
||||||
|
<Text style={styles.packDescription}>{pack.description}</Text>
|
||||||
|
)}
|
||||||
|
<View style={styles.packActions}>
|
||||||
|
<Pressable style={styles.packInfoButton} onPress={onInfoPress}>
|
||||||
|
<Ionicons name="eye-outline" size={18} color={colors.textSecondary} />
|
||||||
|
<Text style={styles.packInfoButtonText}>Preview</Text>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.packToggleButton,
|
||||||
|
isEnabled && styles.packToggleButtonEnabled,
|
||||||
|
isPremiumLocked && styles.packToggleButtonLocked,
|
||||||
|
]}
|
||||||
|
onPress={onToggle}
|
||||||
|
>
|
||||||
|
{isPremiumLocked ? (
|
||||||
|
<>
|
||||||
|
<Ionicons name="lock-closed" size={16} color={colors.premium.gold} />
|
||||||
|
<Text style={[styles.packToggleButtonText, styles.packToggleButtonTextLocked]}>
|
||||||
|
Unlock
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : isEnabled ? (
|
||||||
|
<>
|
||||||
|
<Ionicons name="checkmark" size={16} color={colors.primary} />
|
||||||
|
<Text style={[styles.packToggleButtonText, styles.packToggleButtonTextEnabled]}>
|
||||||
|
Enabled
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Ionicons name="add" size={16} color={colors.text} />
|
||||||
|
<Text style={styles.packToggleButtonText}>Enable</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategorySectionProps {
|
||||||
|
category: Category;
|
||||||
|
packs: QuestionPack[];
|
||||||
|
enabledPackIds: string[];
|
||||||
|
hasPremiumAccess: boolean;
|
||||||
|
expandedPackId: string | null;
|
||||||
|
onPackPress: (packId: string) => void;
|
||||||
|
onPackToggle: (pack: QuestionPack) => void;
|
||||||
|
onPackInfo: (pack: QuestionPack) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CategorySection({
|
||||||
|
category,
|
||||||
|
packs,
|
||||||
|
enabledPackIds,
|
||||||
|
hasPremiumAccess,
|
||||||
|
expandedPackId,
|
||||||
|
onPackPress,
|
||||||
|
onPackToggle,
|
||||||
|
onPackInfo,
|
||||||
|
}: CategorySectionProps) {
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
const enabledCount = packs.filter(p => enabledPackIds.includes(p.id)).length;
|
||||||
|
|
||||||
|
const rotateAnim = useSharedValue(0);
|
||||||
|
|
||||||
|
const chevronStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ rotate: `${rotateAnim.value}deg` }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleToggleCollapse = () => {
|
||||||
|
rotateAnim.value = withTiming(isCollapsed ? 0 : -90, { duration: 200 });
|
||||||
|
setIsCollapsed(!isCollapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.categorySection}>
|
||||||
|
<Pressable style={styles.categoryHeader} onPress={handleToggleCollapse}>
|
||||||
|
<View style={styles.categoryHeaderLeft}>
|
||||||
|
{category.icon && (
|
||||||
|
<Text style={styles.categoryIcon}>{category.icon}</Text>
|
||||||
|
)}
|
||||||
|
<Text style={styles.categoryName}>{category.name}</Text>
|
||||||
|
{enabledCount > 0 && (
|
||||||
|
<View style={styles.categoryBadge}>
|
||||||
|
<Text style={styles.categoryBadgeText}>{enabledCount}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Animated.View style={chevronStyle}>
|
||||||
|
<Ionicons name="chevron-down" size={20} color={colors.textSecondary} />
|
||||||
|
</Animated.View>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.duration(200)}
|
||||||
|
exiting={FadeOut.duration(150)}
|
||||||
|
style={styles.packsList}
|
||||||
|
>
|
||||||
|
{packs.map((pack) => (
|
||||||
|
<PackRow
|
||||||
|
key={pack.id}
|
||||||
|
pack={pack}
|
||||||
|
isEnabled={enabledPackIds.includes(pack.id)}
|
||||||
|
isPremiumLocked={pack.is_premium && !hasPremiumAccess}
|
||||||
|
isExpanded={expandedPackId === pack.id}
|
||||||
|
onPress={() => onPackPress(pack.id)}
|
||||||
|
onToggle={() => onPackToggle(pack)}
|
||||||
|
onInfoPress={() => onPackInfo(pack)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QuestionPacksScreen() {
|
||||||
|
const { packs, categories, enabledPackIds, togglePack, isLoading, fetchPacks } = usePacksStore();
|
||||||
|
const { user, partner } = useAuthStore();
|
||||||
|
const { subscription } = useSubscriptionStore();
|
||||||
|
|
||||||
|
const [expandedPackId, setExpandedPackId] = useState<string | null>(null);
|
||||||
|
const [showPaywall, setShowPaywall] = useState(false);
|
||||||
|
const [showTeaser, setShowTeaser] = useState(false);
|
||||||
|
const [teaserPack, setTeaserPack] = useState<QuestionPack | null>(null);
|
||||||
|
|
||||||
|
const scrollY = useSharedValue(0);
|
||||||
|
|
||||||
|
const scrollHandler = useAnimatedScrollHandler({
|
||||||
|
onScroll: (event) => {
|
||||||
|
scrollY.value = event.contentOffset.y;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasPremiumAccess = useMemo(() => {
|
||||||
|
return user?.is_premium || partner?.is_premium || subscription.isProUser;
|
||||||
|
}, [user?.is_premium, partner?.is_premium, subscription.isProUser]);
|
||||||
|
|
||||||
|
const packsByCategory = useMemo(() => {
|
||||||
|
const grouped = new Map<string, QuestionPack[]>();
|
||||||
|
categories.forEach(cat => grouped.set(cat.id, []));
|
||||||
|
grouped.set("uncategorized", []);
|
||||||
|
packs.forEach(pack => {
|
||||||
|
const catId = pack.category_id || "uncategorized";
|
||||||
|
const existing = grouped.get(catId) || [];
|
||||||
|
existing.push(pack);
|
||||||
|
grouped.set(catId, existing);
|
||||||
|
});
|
||||||
|
return grouped;
|
||||||
|
}, [packs, categories]);
|
||||||
|
|
||||||
|
const handlePackPress = useCallback((packId: string) => {
|
||||||
|
setExpandedPackId(current => current === packId ? null : packId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePackToggle = useCallback(async (pack: QuestionPack) => {
|
||||||
|
if (pack.is_premium && !hasPremiumAccess) {
|
||||||
|
setTeaserPack(pack);
|
||||||
|
setShowTeaser(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await togglePack(pack.id);
|
||||||
|
if (!result.success && result.reason === "no_couple") {
|
||||||
|
Alert.alert(
|
||||||
|
"Pair with Partner",
|
||||||
|
"You need to pair with your partner before you can customise question packs.",
|
||||||
|
[
|
||||||
|
{ text: "Later", style: "cancel" },
|
||||||
|
{ text: "Pair Now", onPress: () => router.push("/pairing") }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [hasPremiumAccess, togglePack]);
|
||||||
|
|
||||||
|
const handlePackInfo = useCallback((pack: QuestionPack) => {
|
||||||
|
const isPremiumLocked = pack.is_premium && !hasPremiumAccess;
|
||||||
|
if (isPremiumLocked) {
|
||||||
|
setTeaserPack(pack);
|
||||||
|
setShowTeaser(true);
|
||||||
|
} else {
|
||||||
|
router.push(`/pack/${pack.id}`);
|
||||||
|
}
|
||||||
|
}, [hasPremiumAccess]);
|
||||||
|
|
||||||
|
const handleTeaserUnlock = useCallback(() => {
|
||||||
|
setShowTeaser(false);
|
||||||
|
setShowPaywall(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTeaserClose = useCallback(() => {
|
||||||
|
setShowTeaser(false);
|
||||||
|
setTeaserPack(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const enabledPackCount = enabledPackIds.length;
|
||||||
|
const totalQuestions = packs
|
||||||
|
.filter(p => enabledPackIds.includes(p.id))
|
||||||
|
.reduce((sum, p) => sum + (p.questions?.[0]?.count || 0), 0);
|
||||||
|
|
||||||
|
// Animated styles for collapsing header
|
||||||
|
const largeHeaderStyle = useAnimatedStyle(() => {
|
||||||
|
const opacity = interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[0, HEADER_SCROLL_DISTANCE * 0.5],
|
||||||
|
[1, 0],
|
||||||
|
Extrapolation.CLAMP
|
||||||
|
);
|
||||||
|
const translateY = interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[0, HEADER_SCROLL_DISTANCE],
|
||||||
|
[0, -20],
|
||||||
|
Extrapolation.CLAMP
|
||||||
|
);
|
||||||
|
return { opacity, transform: [{ translateY }] };
|
||||||
|
});
|
||||||
|
|
||||||
|
const compactHeaderStyle = useAnimatedStyle(() => {
|
||||||
|
const opacity = interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[HEADER_SCROLL_DISTANCE * 0.5, HEADER_SCROLL_DISTANCE],
|
||||||
|
[0, 1],
|
||||||
|
Extrapolation.CLAMP
|
||||||
|
);
|
||||||
|
return { opacity };
|
||||||
|
});
|
||||||
|
|
||||||
|
const navBarBackgroundStyle = useAnimatedStyle(() => {
|
||||||
|
const opacity = interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[0, HEADER_SCROLL_DISTANCE * 0.8],
|
||||||
|
[0, 1],
|
||||||
|
Extrapolation.CLAMP
|
||||||
|
);
|
||||||
|
return { opacity };
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GradientBackground>
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Fixed Nav Bar */}
|
||||||
|
<View style={styles.navBar}>
|
||||||
|
{/* Nav bar background with blur */}
|
||||||
|
<Animated.View style={[styles.navBarBackground, navBarBackgroundStyle]}>
|
||||||
|
{Platform.OS === "ios" ? (
|
||||||
|
<BlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} />
|
||||||
|
) : (
|
||||||
|
<View style={[StyleSheet.absoluteFill, { backgroundColor: "rgba(13, 13, 26, 0.95)" }]} />
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Back button */}
|
||||||
|
<Pressable
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={() => router.back()}
|
||||||
|
hitSlop={8}
|
||||||
|
>
|
||||||
|
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
{/* Compact title (fades in on scroll) */}
|
||||||
|
<Animated.Text style={[styles.navBarTitle, compactHeaderStyle]}>
|
||||||
|
Question Packs
|
||||||
|
</Animated.Text>
|
||||||
|
|
||||||
|
{/* Spacer for alignment */}
|
||||||
|
<View style={styles.navBarSpacer} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Scrollable Content */}
|
||||||
|
<Animated.ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
onScroll={scrollHandler}
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={isLoading}
|
||||||
|
onRefresh={fetchPacks}
|
||||||
|
tintColor={colors.primary}
|
||||||
|
colors={[colors.primary]}
|
||||||
|
progressViewOffset={HEADER_MAX_HEIGHT}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
>
|
||||||
|
{/* Large Header (fades out on scroll) */}
|
||||||
|
<Animated.View style={[styles.largeHeader, largeHeaderStyle]}>
|
||||||
|
<Text style={styles.label}>COLLECTION</Text>
|
||||||
|
<Text style={styles.title}>Question Packs</Text>
|
||||||
|
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.statsBadge}>
|
||||||
|
<Ionicons name="checkmark-circle" size={14} color={colors.primary} />
|
||||||
|
<Text style={styles.statsBadgeText}>
|
||||||
|
{totalQuestions} {totalQuestions === 1 ? "question" : "questions"} from {enabledPackCount} {enabledPackCount === 1 ? "pack" : "packs"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
<View style={styles.categoriesContainer}>
|
||||||
|
{categories.map(category => {
|
||||||
|
const categoryPacks = packsByCategory.get(category.id) || [];
|
||||||
|
if (categoryPacks.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CategorySection
|
||||||
|
key={category.id}
|
||||||
|
category={category}
|
||||||
|
packs={categoryPacks}
|
||||||
|
enabledPackIds={enabledPackIds}
|
||||||
|
hasPremiumAccess={hasPremiumAccess}
|
||||||
|
expandedPackId={expandedPackId}
|
||||||
|
onPackPress={handlePackPress}
|
||||||
|
onPackToggle={handlePackToggle}
|
||||||
|
onPackInfo={handlePackInfo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{(packsByCategory.get("uncategorized")?.length || 0) > 0 && (
|
||||||
|
<CategorySection
|
||||||
|
category={{ id: "uncategorized", name: "Other", description: null, icon: "📁", sort_order: 999, created_at: "" }}
|
||||||
|
packs={packsByCategory.get("uncategorized") || []}
|
||||||
|
enabledPackIds={enabledPackIds}
|
||||||
|
hasPremiumAccess={hasPremiumAccess}
|
||||||
|
expandedPackId={expandedPackId}
|
||||||
|
onPackPress={handlePackPress}
|
||||||
|
onPackToggle={handlePackToggle}
|
||||||
|
onPackInfo={handlePackInfo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Animated.ScrollView>
|
||||||
|
|
||||||
|
{/* Modals */}
|
||||||
|
{teaserPack && (
|
||||||
|
<PackTeaser
|
||||||
|
visible={showTeaser}
|
||||||
|
packId={teaserPack.id}
|
||||||
|
packName={teaserPack.name}
|
||||||
|
packIcon={teaserPack.icon}
|
||||||
|
onClose={handleTeaserClose}
|
||||||
|
onUnlock={handleTeaserUnlock}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Paywall
|
||||||
|
visible={showPaywall}
|
||||||
|
onClose={() => setShowPaywall(false)}
|
||||||
|
onSuccess={() => fetchPacks()}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</GradientBackground>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAV_BAR_HEIGHT = 44;
|
||||||
|
const STATUS_BAR_HEIGHT = 60; // Match other pages
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fixed Nav Bar
|
||||||
|
navBar: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: STATUS_BAR_HEIGHT + NAV_BAR_HEIGHT,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingTop: STATUS_BAR_HEIGHT - 10,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
zIndex: 100,
|
||||||
|
},
|
||||||
|
navBarBackground: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "rgba(255, 255, 255, 0.1)",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
navBarTitle: {
|
||||||
|
flex: 1,
|
||||||
|
...typography.headline,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: "center",
|
||||||
|
marginRight: 40, // Balance with back button
|
||||||
|
},
|
||||||
|
navBarSpacer: {
|
||||||
|
width: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Large Header
|
||||||
|
largeHeader: {
|
||||||
|
alignItems: "center",
|
||||||
|
paddingBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontWeight: "600",
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: colors.primary,
|
||||||
|
textAlign: "center",
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
...typography.title1,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
separatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: "rgba(233, 69, 96, 0.3)",
|
||||||
|
},
|
||||||
|
separatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
transform: [{ rotate: "45deg" }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
statsBadge: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "rgba(233, 69, 96, 0.1)",
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgba(233, 69, 96, 0.2)",
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
statsBadgeText: {
|
||||||
|
...typography.caption1,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Scroll Content
|
||||||
|
scrollContent: {
|
||||||
|
paddingTop: 60, // Match other pages
|
||||||
|
paddingBottom: Platform.OS === "ios" ? 120 : 100,
|
||||||
|
},
|
||||||
|
categoriesContainer: {
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Category Section
|
||||||
|
categorySection: {
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
categoryHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
categoryHeaderLeft: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
categoryIcon: {
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
categoryName: {
|
||||||
|
...typography.headline,
|
||||||
|
color: colors.text,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 1,
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
categoryBadge: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 10,
|
||||||
|
minWidth: 20,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
categoryBadgeText: {
|
||||||
|
...typography.caption2,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
packsList: {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
||||||
|
borderRadius: radius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.glass.border,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pack Row
|
||||||
|
packRow: {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.glass.border,
|
||||||
|
},
|
||||||
|
packRowMain: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: spacing.md,
|
||||||
|
gap: spacing.md,
|
||||||
|
},
|
||||||
|
packIcon: {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.08)",
|
||||||
|
},
|
||||||
|
packIconEnabled: {
|
||||||
|
backgroundColor: "rgba(233, 69, 96, 0.1)",
|
||||||
|
borderColor: "rgba(233, 69, 96, 0.2)",
|
||||||
|
},
|
||||||
|
packIconText: {
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
packContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
packTitleRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: spacing.xs,
|
||||||
|
},
|
||||||
|
packName: {
|
||||||
|
...typography.body,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
packMeta: {
|
||||||
|
...typography.caption1,
|
||||||
|
color: colors.textTertiary,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
packMetaEnabled: {
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Expanded Pack
|
||||||
|
packExpanded: {
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingBottom: spacing.md,
|
||||||
|
paddingTop: 0,
|
||||||
|
},
|
||||||
|
packDescription: {
|
||||||
|
...typography.subhead,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
lineHeight: 20,
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
packActions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
packInfoButton: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: spacing.xs,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
borderRadius: radius.sm,
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.glass.border,
|
||||||
|
},
|
||||||
|
packInfoButtonText: {
|
||||||
|
...typography.subhead,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
packToggleButton: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: spacing.xs,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
borderRadius: radius.sm,
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.12)",
|
||||||
|
},
|
||||||
|
packToggleButtonEnabled: {
|
||||||
|
backgroundColor: "rgba(233, 69, 96, 0.1)",
|
||||||
|
borderColor: "rgba(233, 69, 96, 0.3)",
|
||||||
|
},
|
||||||
|
packToggleButtonLocked: {
|
||||||
|
backgroundColor: "rgba(212, 175, 55, 0.1)",
|
||||||
|
borderColor: "rgba(212, 175, 55, 0.3)",
|
||||||
|
},
|
||||||
|
packToggleButtonText: {
|
||||||
|
...typography.subhead,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
packToggleButtonTextEnabled: {
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
packToggleButtonTextLocked: {
|
||||||
|
color: colors.premium.gold,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -408,7 +408,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
paddingTop: Platform.OS === "ios" ? 60 : 40,
|
paddingTop: 60,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingBottom: spacing.lg,
|
paddingBottom: spacing.lg,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ import { GradientBackground, GlassCard } from "../../src/components/ui";
|
|||||||
import { FeedbackModal } from "../../src/components/FeedbackModal";
|
import { FeedbackModal } from "../../src/components/FeedbackModal";
|
||||||
import { Paywall } from "../../src/components/Paywall";
|
import { Paywall } from "../../src/components/Paywall";
|
||||||
import { Events } from "../../src/lib/analytics";
|
import { Events } from "../../src/lib/analytics";
|
||||||
import { colors, gradients, spacing, radius, typography, shadows } from "../../src/theme";
|
import { colors, gradients, spacing, radius, typography, shadows, featureColors } from "../../src/theme";
|
||||||
|
|
||||||
const MAX_CONTENT_WIDTH = 500;
|
const MAX_CONTENT_WIDTH = 500;
|
||||||
|
const ACCENT_COLOR = featureColors.profile.accent;
|
||||||
|
const ACCENT_GRADIENT = featureColors.profile.gradient as [string, string];
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const { user, partner, couple, signOut, fetchCouple, fetchUser } = useAuthStore();
|
const { user, partner, couple, signOut, fetchCouple, fetchUser } = useAuthStore();
|
||||||
@@ -447,7 +449,13 @@ export default function ProfileScreen() {
|
|||||||
entering={FadeInDown.delay(100).duration(500)}
|
entering={FadeInDown.delay(100).duration(500)}
|
||||||
style={styles.header}
|
style={styles.header}
|
||||||
>
|
>
|
||||||
|
<Text style={styles.headerLabel}>SETTINGS</Text>
|
||||||
<Text style={styles.title}>Profile</Text>
|
<Text style={styles.title}>Profile</Text>
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
@@ -464,7 +472,7 @@ export default function ProfileScreen() {
|
|||||||
style={styles.avatarTouchable}
|
style={styles.avatarTouchable}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={ACCENT_GRADIENT}
|
||||||
style={styles.avatarGradient}
|
style={styles.avatarGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
@@ -560,7 +568,7 @@ export default function ProfileScreen() {
|
|||||||
<View style={styles.rowContainer}>
|
<View style={styles.rowContainer}>
|
||||||
<View style={styles.rowLeft}>
|
<View style={styles.rowLeft}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={ACCENT_GRADIENT}
|
||||||
style={styles.partnerIconGradient}
|
style={styles.partnerIconGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
@@ -590,7 +598,7 @@ export default function ProfileScreen() {
|
|||||||
>
|
>
|
||||||
<View style={styles.rowLeft}>
|
<View style={styles.rowLeft}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={ACCENT_GRADIENT}
|
||||||
style={styles.partnerIconGradient}
|
style={styles.partnerIconGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
@@ -640,7 +648,7 @@ export default function ProfileScreen() {
|
|||||||
<View style={styles.rowContainer}>
|
<View style={styles.rowContainer}>
|
||||||
<View style={styles.rowLeft}>
|
<View style={styles.rowLeft}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={ACCENT_GRADIENT}
|
||||||
style={styles.partnerIconGradient}
|
style={styles.partnerIconGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
@@ -683,7 +691,7 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={ACCENT_GRADIENT}
|
||||||
style={styles.upgradeButton}
|
style={styles.upgradeButton}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 0 }}
|
end={{ x: 1, y: 0 }}
|
||||||
@@ -718,7 +726,7 @@ export default function ProfileScreen() {
|
|||||||
<View style={styles.rowContainer}>
|
<View style={styles.rowContainer}>
|
||||||
<View style={styles.rowLeft}>
|
<View style={styles.rowLeft}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={ACCENT_GRADIENT}
|
||||||
style={styles.partnerIconGradient}
|
style={styles.partnerIconGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
@@ -734,8 +742,8 @@ export default function ProfileScreen() {
|
|||||||
value={showExplicit}
|
value={showExplicit}
|
||||||
onValueChange={handleExplicitToggle}
|
onValueChange={handleExplicitToggle}
|
||||||
disabled={isUpdatingExplicit}
|
disabled={isUpdatingExplicit}
|
||||||
trackColor={{ false: colors.glass.border, true: colors.primaryLight }}
|
trackColor={{ false: colors.glass.border, true: colors.secondaryLight }}
|
||||||
thumbColor={showExplicit ? colors.primary : colors.textTertiary}
|
thumbColor={showExplicit ? ACCENT_COLOR : colors.textTertiary}
|
||||||
ios_backgroundColor={colors.glass.border}
|
ios_backgroundColor={colors.glass.border}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -745,7 +753,7 @@ export default function ProfileScreen() {
|
|||||||
<View style={styles.rowContainer}>
|
<View style={styles.rowContainer}>
|
||||||
<View style={styles.rowLeft}>
|
<View style={styles.rowLeft}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={ACCENT_GRADIENT}
|
||||||
style={styles.partnerIconGradient}
|
style={styles.partnerIconGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
@@ -765,8 +773,8 @@ export default function ProfileScreen() {
|
|||||||
value={biometricEnabled}
|
value={biometricEnabled}
|
||||||
onValueChange={handleBiometricToggle}
|
onValueChange={handleBiometricToggle}
|
||||||
disabled={isUpdatingBiometric}
|
disabled={isUpdatingBiometric}
|
||||||
trackColor={{ false: colors.glass.border, true: colors.primaryLight }}
|
trackColor={{ false: colors.glass.border, true: colors.secondaryLight }}
|
||||||
thumbColor={biometricEnabled ? colors.primary : colors.textTertiary}
|
thumbColor={biometricEnabled ? ACCENT_COLOR : colors.textTertiary}
|
||||||
ios_backgroundColor={colors.glass.border}
|
ios_backgroundColor={colors.glass.border}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -812,7 +820,7 @@ export default function ProfileScreen() {
|
|||||||
>
|
>
|
||||||
<View style={styles.rowLeft}>
|
<View style={styles.rowLeft}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={ACCENT_GRADIENT}
|
||||||
style={styles.partnerIconGradient}
|
style={styles.partnerIconGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
@@ -997,7 +1005,7 @@ export default function ProfileScreen() {
|
|||||||
style={styles.versionContainer}
|
style={styles.versionContainer}
|
||||||
>
|
>
|
||||||
<View style={styles.versionBadge}>
|
<View style={styles.versionBadge}>
|
||||||
<Ionicons name="heart" size={12} color={colors.primary} />
|
<Ionicons name="heart" size={12} color={ACCENT_COLOR} />
|
||||||
<Text style={styles.version}>Sauci v1.0.0</Text>
|
<Text style={styles.version}>Sauci v1.0.0</Text>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -1125,11 +1133,40 @@ const styles = StyleSheet.create({
|
|||||||
header: {
|
header: {
|
||||||
paddingTop: 60,
|
paddingTop: 60,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingBottom: spacing.lg,
|
paddingBottom: spacing.md,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
headerLabel: {
|
||||||
|
...typography.caption2,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: colors.secondary,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
...typography.title1,
|
...typography.largeTitle,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
separatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(155, 89, 182, 0.3)',
|
||||||
|
},
|
||||||
|
separatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: colors.secondary,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.sm,
|
||||||
|
opacity: 0.6,
|
||||||
},
|
},
|
||||||
profileSection: {
|
profileSection: {
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
@@ -1186,7 +1223,7 @@ const styles = StyleSheet.create({
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.secondary,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
@@ -1227,7 +1264,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: colors.glass.background,
|
backgroundColor: colors.glass.background,
|
||||||
borderRadius: radius.md,
|
borderRadius: radius.md,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.primary,
|
borderColor: colors.secondary,
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
@@ -1250,7 +1287,7 @@ const styles = StyleSheet.create({
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.secondary,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
@@ -1364,7 +1401,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
manageButtonText: {
|
manageButtonText: {
|
||||||
...typography.subhead,
|
...typography.subhead,
|
||||||
color: colors.primary,
|
color: colors.secondary,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
upgradeButton: {
|
upgradeButton: {
|
||||||
|
|||||||
@@ -33,11 +33,6 @@ export default function QuizScreen() {
|
|||||||
entering={FadeInDown.duration(600).springify()}
|
entering={FadeInDown.duration(600).springify()}
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
|
||||||
<View style={styles.iconContainer}>
|
|
||||||
<Ionicons name="heart" size={36} color={ACCENT_COLOR} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Title section */}
|
{/* Title section */}
|
||||||
<Text style={styles.label}>INTIMACY</Text>
|
<Text style={styles.label}>INTIMACY</Text>
|
||||||
<Text style={styles.title}>Quiz</Text>
|
<Text style={styles.title}>Quiz</Text>
|
||||||
@@ -81,12 +76,10 @@ export default function QuizScreen() {
|
|||||||
{/* Bottom teaser */}
|
{/* Bottom teaser */}
|
||||||
<Text style={styles.teaser}>Something special is being crafted for you</Text>
|
<Text style={styles.teaser}>Something special is being crafted for you</Text>
|
||||||
|
|
||||||
{/* Interest button */}
|
{/* Interest button - always reserve space to prevent layout shift */}
|
||||||
|
<View style={styles.interestButtonContainer}>
|
||||||
{isAuthenticated && !isLoading && (
|
{isAuthenticated && !isLoading && (
|
||||||
<Animated.View
|
<Animated.View entering={FadeIn.delay(500).duration(400)}>
|
||||||
entering={FadeIn.delay(500).duration(400)}
|
|
||||||
style={styles.interestButtonContainer}
|
|
||||||
>
|
|
||||||
<AnimatedPressable
|
<AnimatedPressable
|
||||||
onPress={toggleInterest}
|
onPress={toggleInterest}
|
||||||
onPressIn={handlePressIn}
|
onPressIn={handlePressIn}
|
||||||
@@ -115,6 +108,7 @@ export default function QuizScreen() {
|
|||||||
</AnimatedPressable>
|
</AnimatedPressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</GradientBackground>
|
</GradientBackground>
|
||||||
@@ -127,7 +121,7 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingTop: Platform.OS === 'ios' ? 60 : 40,
|
paddingTop: 60,
|
||||||
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
@@ -135,17 +129,6 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
},
|
},
|
||||||
iconContainer: {
|
|
||||||
width: 72,
|
|
||||||
height: 72,
|
|
||||||
borderRadius: 36,
|
|
||||||
backgroundColor: 'rgba(232, 164, 174, 0.1)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(232, 164, 174, 0.2)',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: spacing.xl,
|
|
||||||
},
|
|
||||||
label: {
|
label: {
|
||||||
...typography.caption1,
|
...typography.caption1,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
@@ -223,6 +206,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
interestButtonContainer: {
|
interestButtonContainer: {
|
||||||
marginTop: spacing.xl,
|
marginTop: spacing.xl,
|
||||||
|
minHeight: 44,
|
||||||
},
|
},
|
||||||
interestButton: {
|
interestButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import { usePacksStore, useAuthStore } from "../../src/store";
|
|||||||
import { skipQuestion, getSkippedQuestionIds } from "../../src/lib/skippedQuestions";
|
import { skipQuestion, getSkippedQuestionIds } from "../../src/lib/skippedQuestions";
|
||||||
import { hasSeenSwipeTutorial, markSwipeTutorialSeen } from "../../src/lib/swipeTutorialSeen";
|
import { hasSeenSwipeTutorial, markSwipeTutorialSeen } from "../../src/lib/swipeTutorialSeen";
|
||||||
import { invokeWithAuthRetry } from "../../src/lib/authErrorHandler";
|
import { invokeWithAuthRetry } from "../../src/lib/authErrorHandler";
|
||||||
import SwipeCard from "../../src/components/SwipeCard";
|
// import SwipeCard from "../../src/components/SwipeCard";
|
||||||
|
import SwipeCard from "../../src/components/SwipeCardPremium"; // PoC: Premium styling
|
||||||
import SwipeTutorial from "../../src/components/SwipeTutorial";
|
import SwipeTutorial from "../../src/components/SwipeTutorial";
|
||||||
import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui";
|
import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui";
|
||||||
import { Events } from "../../src/lib/analytics";
|
import { Events } from "../../src/lib/analytics";
|
||||||
@@ -115,6 +116,7 @@ export default function SwipeScreen() {
|
|||||||
const checkAnswerGap = async (): Promise<boolean> => {
|
const checkAnswerGap = async (): Promise<boolean> => {
|
||||||
if (!couple || !partner) {
|
if (!couple || !partner) {
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
|
setGapInfo(null);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,10 +131,15 @@ export default function SwipeScreen() {
|
|||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
const result = data[0];
|
const result = data[0];
|
||||||
setIsBlocked(result.is_blocked);
|
setIsBlocked(result.is_blocked);
|
||||||
|
// Only set gap info if threshold is enabled (> 0)
|
||||||
|
if (result.threshold > 0) {
|
||||||
setGapInfo({
|
setGapInfo({
|
||||||
unanswered: result.unanswered_by_partner,
|
unanswered: result.unanswered_by_partner,
|
||||||
threshold: result.threshold
|
threshold: result.threshold
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
setGapInfo(null);
|
||||||
|
}
|
||||||
return result.is_blocked;
|
return result.is_blocked;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -141,6 +148,18 @@ export default function SwipeScreen() {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate effective total questions (accounting for gap limit)
|
||||||
|
const effectiveTotal = (() => {
|
||||||
|
if (!gapInfo || gapInfo.threshold === 0) {
|
||||||
|
// No gap limit active, show all questions
|
||||||
|
return questions.length;
|
||||||
|
}
|
||||||
|
// remaining = how many more questions until blocked
|
||||||
|
const remaining = gapInfo.threshold - gapInfo.unanswered;
|
||||||
|
// effective total = current position + remaining (capped by actual questions)
|
||||||
|
return Math.min(questions.length, currentIndex + Math.max(0, remaining));
|
||||||
|
})();
|
||||||
|
|
||||||
// Track when all questions are exhausted
|
// Track when all questions are exhausted
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (questions.length > 0 && currentIndex >= questions.length && !hasTrackedExhausted.current) {
|
if (questions.length > 0 && currentIndex >= questions.length && !hasTrackedExhausted.current) {
|
||||||
@@ -193,12 +212,14 @@ export default function SwipeScreen() {
|
|||||||
// Filter out recently skipped questions
|
// Filter out recently skipped questions
|
||||||
const filtered = (data || []).filter((q: any) => !skippedIds.has(q.id));
|
const filtered = (data || []).filter((q: any) => !skippedIds.has(q.id));
|
||||||
|
|
||||||
// If no questions returned and user has partner, check if it's due to answer gap
|
// Always check answer gap when user has partner and not viewing specific pack
|
||||||
if (filtered.length === 0 && partner && !packId) {
|
// This gives us the gap info for accurate counter display
|
||||||
|
if (partner && !packId) {
|
||||||
await checkAnswerGap();
|
await checkAnswerGap();
|
||||||
} else {
|
} else {
|
||||||
// Reset blocked state if we have questions
|
// Reset blocked state if no partner or viewing specific pack
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
|
setGapInfo(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sorted = filtered.sort((a: any, b: any) => {
|
const sorted = filtered.sort((a: any, b: any) => {
|
||||||
@@ -554,10 +575,10 @@ export default function SwipeScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientBackground>
|
<GradientBackground>
|
||||||
{/* Ambient Orbs */}
|
{/* Ambient Orbs - Premium gold/rose */}
|
||||||
<Animated.View style={[styles.ambientOrb, styles.orbTopRight, orbStyle1]} pointerEvents="none">
|
<Animated.View style={[styles.ambientOrb, styles.orbTopRight, orbStyle1]} pointerEvents="none">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[colors.primaryGlow, 'transparent']}
|
colors={[colors.premium.goldGlow, 'transparent']}
|
||||||
style={styles.orbGradient}
|
style={styles.orbGradient}
|
||||||
start={{ x: 0.5, y: 0.5 }}
|
start={{ x: 0.5, y: 0.5 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
@@ -565,7 +586,7 @@ export default function SwipeScreen() {
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
<Animated.View style={[styles.ambientOrb, styles.orbBottomLeft, orbStyle2]} pointerEvents="none">
|
<Animated.View style={[styles.ambientOrb, styles.orbBottomLeft, orbStyle2]} pointerEvents="none">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[colors.secondaryGlow, 'transparent']}
|
colors={['rgba(232, 164, 174, 0.2)', 'transparent']}
|
||||||
style={styles.orbGradient}
|
style={styles.orbGradient}
|
||||||
start={{ x: 0.5, y: 0.5 }}
|
start={{ x: 0.5, y: 0.5 }}
|
||||||
end={{ x: 0, y: 0 }}
|
end={{ x: 0, y: 0 }}
|
||||||
@@ -579,26 +600,23 @@ export default function SwipeScreen() {
|
|||||||
style={styles.header}
|
style={styles.header}
|
||||||
>
|
>
|
||||||
<View style={styles.progressContainer}>
|
<View style={styles.progressContainer}>
|
||||||
|
{/* Premium label */}
|
||||||
|
<Text style={styles.progressLabel}>EXPLORE</Text>
|
||||||
<Text style={styles.progressText}>
|
<Text style={styles.progressText}>
|
||||||
{currentIndex + 1} of {questions.length}
|
{currentIndex + 1} of {effectiveTotal || questions.length}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[styles.progressBar, styles.progressBarEnhanced]}>
|
<View style={[styles.progressBar, styles.progressBarPremium]}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.progressFill,
|
styles.progressFill,
|
||||||
{ width: `${((currentIndex + 1) / questions.length) * 100}%` }
|
styles.progressFillPremium,
|
||||||
|
{ width: `${((currentIndex + 1) / (effectiveTotal || questions.length || 1)) * 100}%` }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
|
||||||
colors={gradients.primary as [string, string]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 0 }}
|
|
||||||
style={styles.progressGradient}
|
|
||||||
/>
|
|
||||||
{/* Shimmer sweep */}
|
{/* Shimmer sweep */}
|
||||||
<Animated.View style={[styles.progressShimmer, shimmerStyle]}>
|
<Animated.View style={[styles.progressShimmer, shimmerStyle]}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['transparent', 'rgba(255,255,255,0.3)', 'transparent']}
|
colors={['transparent', 'rgba(212, 175, 55, 0.5)', 'transparent']}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 0 }}
|
end={{ x: 1, y: 0 }}
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
@@ -629,12 +647,12 @@ export default function SwipeScreen() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Hint */}
|
{/* Hint - Premium styling */}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeIn.delay(500).duration(400)}
|
entering={FadeIn.delay(500).duration(400)}
|
||||||
style={styles.hintContainer}
|
style={styles.hintContainer}
|
||||||
>
|
>
|
||||||
<Text style={styles.hintText}>Swipe or tap to answer</Text>
|
<Text style={styles.hintTextPremium}>Swipe or tap to answer</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* Bottom spacing for tab bar */}
|
{/* Bottom spacing for tab bar */}
|
||||||
@@ -686,6 +704,13 @@ const styles = StyleSheet.create({
|
|||||||
progressContainer: {
|
progressContainer: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
progressLabel: {
|
||||||
|
...typography.caption2,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: colors.premium.gold,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
progressText: {
|
progressText: {
|
||||||
...typography.subhead,
|
...typography.subhead,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
@@ -698,18 +723,23 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
progressBarEnhanced: {
|
progressBarPremium: {
|
||||||
height: 8,
|
width: 140,
|
||||||
borderRadius: 4,
|
height: 4,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.glass.borderLight,
|
borderColor: 'rgba(212, 175, 55, 0.15)',
|
||||||
...shadows.glow(colors.primaryGlow),
|
|
||||||
},
|
},
|
||||||
progressFill: {
|
progressFill: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
|
progressFillPremium: {
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.5)',
|
||||||
|
borderRadius: 2,
|
||||||
|
},
|
||||||
progressGradient: {
|
progressGradient: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
@@ -725,24 +755,24 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
// Card stack
|
// Card stack - Premium styling
|
||||||
stackCard: {
|
stackCard: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
width: "85%",
|
width: "85%",
|
||||||
height: 480,
|
height: 500,
|
||||||
backgroundColor: colors.glass.backgroundLight,
|
backgroundColor: '#0d0d1a',
|
||||||
borderRadius: radius.xxl,
|
borderRadius: radius.xl,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.glass.border,
|
borderColor: 'rgba(212, 175, 55, 0.15)',
|
||||||
},
|
},
|
||||||
stackCardSecond: {
|
stackCardSecond: {
|
||||||
transform: [{ scale: 0.95 }, { translateY: 12 }],
|
transform: [{ scale: 0.95 }, { translateY: 12 }],
|
||||||
opacity: 0.5,
|
opacity: 0.4,
|
||||||
...shadows.md,
|
...shadows.md,
|
||||||
},
|
},
|
||||||
stackCardThird: {
|
stackCardThird: {
|
||||||
transform: [{ scale: 0.90 }, { translateY: 24 }],
|
transform: [{ scale: 0.90 }, { translateY: 24 }],
|
||||||
opacity: 0.25,
|
opacity: 0.2,
|
||||||
...shadows.sm,
|
...shadows.sm,
|
||||||
},
|
},
|
||||||
hintContainer: {
|
hintContainer: {
|
||||||
@@ -753,6 +783,12 @@ const styles = StyleSheet.create({
|
|||||||
...typography.caption1,
|
...typography.caption1,
|
||||||
color: colors.textTertiary,
|
color: colors.textTertiary,
|
||||||
},
|
},
|
||||||
|
hintTextPremium: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
color: colors.textTertiary,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
bottomSpacer: {
|
bottomSpacer: {
|
||||||
height: Platform.OS === 'ios' ? 100 : 80,
|
height: Platform.OS === 'ios' ? 100 : 80,
|
||||||
},
|
},
|
||||||
|
|||||||
+415
-191
@@ -6,9 +6,16 @@ import { BlurView } from "expo-blur";
|
|||||||
import { supabase } from "../../src/lib/supabase";
|
import { supabase } from "../../src/lib/supabase";
|
||||||
import { usePacksStore, useSubscriptionStore, useAuthStore } from "../../src/store";
|
import { usePacksStore, useSubscriptionStore, useAuthStore } from "../../src/store";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import Animated, { FadeInDown, FadeIn } from "react-native-reanimated";
|
import Animated, {
|
||||||
import { GradientBackground, GlassCard, ShimmerEffect } from "../../src/components/ui";
|
FadeInDown,
|
||||||
import { colors, gradients, spacing, radius, typography, blur, shadows } from "../../src/theme";
|
useSharedValue,
|
||||||
|
useAnimatedScrollHandler,
|
||||||
|
useAnimatedStyle,
|
||||||
|
interpolate,
|
||||||
|
Extrapolation,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
import { GradientBackground, GlassCard } from "../../src/components/ui";
|
||||||
|
import { colors, gradients, spacing, radius, typography } from "../../src/theme";
|
||||||
import { Question } from "../../src/types";
|
import { Question } from "../../src/types";
|
||||||
|
|
||||||
type TooltipType = 'initiator' | 'coupleType' | null;
|
type TooltipType = 'initiator' | 'coupleType' | null;
|
||||||
@@ -24,13 +31,14 @@ const TOOLTIP_CONTENT = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Boutique gradient for question cards
|
const ACCENT_COLOR = colors.primary;
|
||||||
const QUESTION_GRADIENTS: Array<readonly [string, string]> = [
|
|
||||||
['rgba(233, 69, 96, 0.15)', 'rgba(155, 89, 182, 0.15)'],
|
const NAV_BAR_HEIGHT = 44;
|
||||||
['rgba(155, 89, 182, 0.15)', 'rgba(233, 69, 96, 0.15)'],
|
const STATUS_BAR_HEIGHT = 60;
|
||||||
['rgba(183, 110, 121, 0.12)', 'rgba(139, 69, 87, 0.12)'],
|
const HERO_HEIGHT = 260;
|
||||||
['rgba(142, 68, 173, 0.12)', 'rgba(44, 62, 80, 0.12)'],
|
const HEADER_SCROLL_DISTANCE = 120;
|
||||||
];
|
|
||||||
|
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList<Question>);
|
||||||
|
|
||||||
export default function PackDetailsScreen() {
|
export default function PackDetailsScreen() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
@@ -42,11 +50,12 @@ export default function PackDetailsScreen() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [activeTooltip, setActiveTooltip] = useState<TooltipType>(null);
|
const [activeTooltip, setActiveTooltip] = useState<TooltipType>(null);
|
||||||
|
|
||||||
|
const scrollY = useSharedValue(0);
|
||||||
|
|
||||||
const pack = packs.find((p) => p.id === id);
|
const pack = packs.find((p) => p.id === id);
|
||||||
const hasPremiumAccess = user?.is_premium || partner?.is_premium || subscription.isProUser;
|
const hasPremiumAccess = user?.is_premium || partner?.is_premium || subscription.isProUser;
|
||||||
const isPremium = pack?.is_premium && hasPremiumAccess;
|
const isPremium = pack?.is_premium && hasPremiumAccess;
|
||||||
|
|
||||||
// Get category for breadcrumb
|
|
||||||
const { categories } = usePacksStore();
|
const { categories } = usePacksStore();
|
||||||
const category = categories.find(c => c.id === pack?.category_id);
|
const category = categories.find(c => c.id === pack?.category_id);
|
||||||
|
|
||||||
@@ -74,23 +83,57 @@ export default function PackDetailsScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrollHandler = useAnimatedScrollHandler({
|
||||||
|
onScroll: (event) => {
|
||||||
|
scrollY.value = event.contentOffset.y;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animated styles for collapsing header
|
||||||
|
const heroStyle = useAnimatedStyle(() => {
|
||||||
|
const opacity = interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[0, HEADER_SCROLL_DISTANCE * 0.7],
|
||||||
|
[1, 0],
|
||||||
|
Extrapolation.CLAMP
|
||||||
|
);
|
||||||
|
const scale = interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[0, HEADER_SCROLL_DISTANCE],
|
||||||
|
[1, 0.95],
|
||||||
|
Extrapolation.CLAMP
|
||||||
|
);
|
||||||
|
return { opacity, transform: [{ scale }] };
|
||||||
|
});
|
||||||
|
|
||||||
|
const compactHeaderStyle = useAnimatedStyle(() => {
|
||||||
|
const opacity = interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[HEADER_SCROLL_DISTANCE * 0.5, HEADER_SCROLL_DISTANCE],
|
||||||
|
[0, 1],
|
||||||
|
Extrapolation.CLAMP
|
||||||
|
);
|
||||||
|
return { opacity };
|
||||||
|
});
|
||||||
|
|
||||||
|
const navBarBackgroundStyle = useAnimatedStyle(() => {
|
||||||
|
const opacity = interpolate(
|
||||||
|
scrollY.value,
|
||||||
|
[0, HEADER_SCROLL_DISTANCE * 0.8],
|
||||||
|
[0, 1],
|
||||||
|
Extrapolation.CLAMP
|
||||||
|
);
|
||||||
|
return { opacity };
|
||||||
|
});
|
||||||
|
|
||||||
const useBlur = Platform.OS === 'ios';
|
const useBlur = Platform.OS === 'ios';
|
||||||
|
|
||||||
const renderItem = ({ item, index }: { item: Question; index: number }) => {
|
const renderItem = ({ item, index }: { item: Question; index: number }) => {
|
||||||
const cardGradient = QUESTION_GRADIENTS[index % QUESTION_GRADIENTS.length];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View entering={FadeInDown.delay(index * 50).duration(300)}>
|
<Animated.View entering={FadeInDown.delay(index * 50).duration(300)}>
|
||||||
<GlassCard style={styles.questionCard}>
|
<View style={styles.questionCard}>
|
||||||
{/* Subtle gradient overlay */}
|
|
||||||
<LinearGradient
|
|
||||||
colors={cardGradient}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={styles.questionContent}>
|
<View style={styles.questionContent}>
|
||||||
|
<Text style={styles.questionNumber}>#{index + 1}</Text>
|
||||||
<Text style={styles.questionText}>{item.text}</Text>
|
<Text style={styles.questionText}>{item.text}</Text>
|
||||||
|
|
||||||
{item.partner_text && (
|
{item.partner_text && (
|
||||||
@@ -101,41 +144,37 @@ export default function PackDetailsScreen() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.footerContainer}>
|
<View style={styles.footerContainer}>
|
||||||
{/* Intensity */}
|
|
||||||
<View style={styles.intensityContainer}>
|
<View style={styles.intensityContainer}>
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
key={i}
|
key={i}
|
||||||
name="flame"
|
name="flame"
|
||||||
size={14}
|
size={12}
|
||||||
color={i < item.intensity ? colors.primary : colors.textTertiary}
|
color={i < item.intensity ? ACCENT_COLOR : 'rgba(255, 255, 255, 0.15)'}
|
||||||
style={{ marginRight: 2 }}
|
style={{ marginRight: 2 }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.badgesContainer}>
|
<View style={styles.badgesContainer}>
|
||||||
{/* Initiator indicator */}
|
|
||||||
{item.target_user_genders && item.target_user_genders.length > 0 && (
|
{item.target_user_genders && item.target_user_genders.length > 0 && (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.targetBadge}
|
style={styles.targetBadge}
|
||||||
onPress={() => setActiveTooltip('initiator')}
|
onPress={() => setActiveTooltip('initiator')}
|
||||||
>
|
>
|
||||||
<Ionicons name="arrow-forward" size={12} color={colors.premium.gold} style={{ marginRight: 4 }} />
|
<Ionicons name="arrow-forward" size={10} color={colors.textTertiary} style={{ marginRight: 2 }} />
|
||||||
{item.target_user_genders.map((g, i) => (
|
{item.target_user_genders.map((g, i) => (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
key={g}
|
key={g}
|
||||||
name={g === 'male' ? 'male' : g === 'female' ? 'female' : 'person'}
|
name={g === 'male' ? 'male' : g === 'female' ? 'female' : 'person'}
|
||||||
size={14}
|
size={12}
|
||||||
color={colors.premium.gold}
|
color={colors.textTertiary}
|
||||||
style={i > 0 ? { marginLeft: 4 } : {}}
|
style={i > 0 ? { marginLeft: 2 } : {}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Ionicons name="help-circle-outline" size={12} color={colors.premium.gold} style={{ marginLeft: 4 }} />
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Couple type indicator */}
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.targetBadge}
|
style={styles.targetBadge}
|
||||||
onPress={() => setActiveTooltip('coupleType')}
|
onPress={() => setActiveTooltip('coupleType')}
|
||||||
@@ -157,8 +196,8 @@ export default function PackDetailsScreen() {
|
|||||||
<Ionicons
|
<Ionicons
|
||||||
key={i}
|
key={i}
|
||||||
name={icon}
|
name={icon}
|
||||||
size={14}
|
size={12}
|
||||||
color={colors.textSecondary}
|
color={colors.textTertiary}
|
||||||
style={i > 0 ? { marginLeft: 2 } : {}}
|
style={i > 0 ? { marginLeft: 2 } : {}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -166,18 +205,109 @@ export default function PackDetailsScreen() {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<Ionicons name="people" size={14} color={colors.textSecondary} />
|
<Ionicons name="people" size={12} color={colors.textTertiary} />
|
||||||
)}
|
)}
|
||||||
<Ionicons name="help-circle-outline" size={12} color={colors.textTertiary} style={{ marginLeft: 4 }} />
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</GlassCard>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const heroContent = (
|
||||||
|
<View style={styles.heroInner}>
|
||||||
|
{/* Premium Banner */}
|
||||||
|
{pack?.is_premium && (
|
||||||
|
<View style={styles.premiumBanner}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
<View style={styles.premiumBannerContent}>
|
||||||
|
<Ionicons name="diamond" size={16} color={colors.background} />
|
||||||
|
<Text style={styles.premiumBannerText}>PREMIUM COLLECTION</Text>
|
||||||
|
<Ionicons name="diamond" size={16} color={colors.background} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.heroContent}>
|
||||||
|
{/* Category label */}
|
||||||
|
<Text style={[styles.label, pack?.is_premium && styles.labelPremium]}>
|
||||||
|
{category ? category.name.toUpperCase() : 'PREVIEW'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Icon with enhanced glow for premium */}
|
||||||
|
<View style={styles.iconWrapper}>
|
||||||
|
{pack?.is_premium && (
|
||||||
|
<>
|
||||||
|
<View style={styles.iconGlowOuter} />
|
||||||
|
<View style={styles.iconGlow} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<View style={[
|
||||||
|
styles.iconContainer,
|
||||||
|
pack?.is_premium && styles.iconContainerPremium
|
||||||
|
]}>
|
||||||
|
{pack?.is_premium && (
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(212, 175, 55, 0.2)', 'rgba(184, 134, 11, 0.1)']}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text style={styles.emoji}>{pack?.icon || "📦"}</Text>
|
||||||
|
</View>
|
||||||
|
{/* Crown for premium */}
|
||||||
|
{pack?.is_premium && (
|
||||||
|
<View style={styles.crownBadge}>
|
||||||
|
<Ionicons name="star" size={14} color={colors.premium.gold} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<Text style={[styles.title, pack?.is_premium && styles.titlePremium]}>
|
||||||
|
{pack?.name || "Pack Details"}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Premium badge - larger and more prominent */}
|
||||||
|
{pack?.is_premium && (
|
||||||
|
<View style={styles.proBadge}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
style={[StyleSheet.absoluteFill, { borderRadius: radius.full }]}
|
||||||
|
/>
|
||||||
|
<Ionicons name="star" size={12} color={colors.background} />
|
||||||
|
<Text style={styles.proBadgeText}>PREMIUM</Text>
|
||||||
|
<Ionicons name="star" size={12} color={colors.background} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={[styles.separatorLine, pack?.is_premium && styles.separatorLinePremium]} />
|
||||||
|
<View style={[styles.separatorDiamond, pack?.is_premium && styles.separatorDiamondPremium]} />
|
||||||
|
<View style={[styles.separatorLine, pack?.is_premium && styles.separatorLinePremium]} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Text style={styles.description}>{pack?.description}</Text>
|
||||||
|
|
||||||
|
{/* Stats badge */}
|
||||||
|
<View style={[styles.statsBadge, pack?.is_premium && styles.statsBadgePremium]}>
|
||||||
|
<Ionicons name="help-circle-outline" size={14} color={pack?.is_premium ? colors.premium.gold : ACCENT_COLOR} />
|
||||||
|
<Text style={[styles.statsText, pack?.is_premium && styles.statsTextPremium]}>{questions.length} questions</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<GradientBackground>
|
<GradientBackground>
|
||||||
@@ -188,103 +318,46 @@ export default function PackDetailsScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const heroContent = (
|
|
||||||
<View style={styles.heroInner}>
|
|
||||||
{/* Background gradient */}
|
|
||||||
<LinearGradient
|
|
||||||
colors={isPremium ? gradients.boutiqueGold as [string, string] : gradients.boutiqueRose as [string, string]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Glass overlay */}
|
|
||||||
{useBlur ? (
|
|
||||||
<BlurView
|
|
||||||
intensity={blur.light}
|
|
||||||
tint="dark"
|
|
||||||
style={[StyleSheet.absoluteFill, styles.heroBlur]}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<View style={[StyleSheet.absoluteFill, styles.heroBlurFallback]} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Silk texture */}
|
|
||||||
<LinearGradient
|
|
||||||
colors={gradients.silkLight as [string, string]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={[StyleSheet.absoluteFill, { opacity: 0.5 }]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Back button */}
|
|
||||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
|
||||||
<View style={styles.backButtonInner}>
|
|
||||||
<Ionicons name="arrow-back" size={22} color={colors.text} />
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* Pack info */}
|
|
||||||
<View style={styles.heroContent}>
|
|
||||||
<View style={[
|
|
||||||
styles.iconContainer,
|
|
||||||
isPremium && styles.iconContainerPremium
|
|
||||||
]}>
|
|
||||||
<Text style={styles.emoji}>{pack?.icon || "📦"}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{pack?.is_premium && (
|
|
||||||
<View style={styles.proBadge}>
|
|
||||||
<Ionicons name="star" size={12} color={colors.text} />
|
|
||||||
<Text style={styles.proBadgeText}>PRO</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Category breadcrumb - tappable to filter by category */}
|
|
||||||
{category && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.breadcrumb}
|
|
||||||
onPress={() => router.replace(`/(app)/packs?category=${category.id}`)}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text style={styles.breadcrumbText}>{category.icon} {category.name}</Text>
|
|
||||||
<Ionicons name="chevron-forward" size={12} color={colors.textTertiary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Text style={styles.title}>{pack?.name || "Pack Details"}</Text>
|
|
||||||
<Text style={styles.description}>{pack?.description}</Text>
|
|
||||||
|
|
||||||
<View style={styles.statsBadge}>
|
|
||||||
<Text style={styles.statsText}>{questions.length} questions</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientBackground>
|
<GradientBackground>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* Hero Section */}
|
{/* Fixed Nav Bar */}
|
||||||
<Animated.View entering={FadeIn.duration(400)}>
|
<View style={styles.navBar}>
|
||||||
{isPremium ? (
|
<Animated.View style={[styles.navBarBackground, navBarBackgroundStyle]}>
|
||||||
<ShimmerEffect enabled={true} style={styles.heroContainer}>
|
{Platform.OS === "ios" ? (
|
||||||
{heroContent}
|
<BlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} />
|
||||||
</ShimmerEffect>
|
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.heroContainer}>
|
<View style={[StyleSheet.absoluteFill, { backgroundColor: "rgba(13, 13, 26, 0.95)" }]} />
|
||||||
{heroContent}
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* Questions List */}
|
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||||
<FlatList
|
<View style={styles.backButtonInner}>
|
||||||
|
<Ionicons name="chevron-back" size={22} color={colors.text} />
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Animated.Text style={[styles.navBarTitle, compactHeaderStyle]} numberOfLines={1}>
|
||||||
|
{pack?.name || "Pack Details"}
|
||||||
|
</Animated.Text>
|
||||||
|
|
||||||
|
<View style={styles.navBarSpacer} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Questions List with Hero Header */}
|
||||||
|
<AnimatedFlatList
|
||||||
data={questions}
|
data={questions}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
contentContainerStyle={styles.list}
|
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
onScroll={scrollHandler}
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
contentContainerStyle={styles.list}
|
||||||
|
ListHeaderComponent={
|
||||||
|
<Animated.View style={[styles.heroContainer, heroStyle]}>
|
||||||
|
{heroContent}
|
||||||
|
</Animated.View>
|
||||||
|
}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<Animated.View entering={FadeInDown.delay(100).duration(300)} style={styles.emptyContainer}>
|
<Animated.View entering={FadeInDown.delay(100).duration(300)} style={styles.emptyContainer}>
|
||||||
<Ionicons name="help-circle-outline" size={48} color={colors.textTertiary} />
|
<Ionicons name="help-circle-outline" size={48} color={colors.textTertiary} />
|
||||||
@@ -345,92 +418,224 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
heroContainer: {
|
|
||||||
borderBottomLeftRadius: radius.xl,
|
// Fixed Nav Bar
|
||||||
borderBottomRightRadius: radius.xl,
|
navBar: {
|
||||||
overflow: 'hidden',
|
position: "absolute",
|
||||||
...shadows.lg,
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: STATUS_BAR_HEIGHT + NAV_BAR_HEIGHT,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingTop: STATUS_BAR_HEIGHT - 10,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
zIndex: 100,
|
||||||
},
|
},
|
||||||
heroInner: {
|
navBarBackground: {
|
||||||
paddingTop: 50,
|
...StyleSheet.absoluteFillObject,
|
||||||
paddingBottom: spacing.lg,
|
borderBottomWidth: 1,
|
||||||
minHeight: 260,
|
borderBottomColor: 'rgba(233, 69, 96, 0.15)',
|
||||||
},
|
overflow: "hidden",
|
||||||
heroBlur: {
|
|
||||||
backgroundColor: 'rgba(22, 33, 62, 0.4)',
|
|
||||||
},
|
|
||||||
heroBlurFallback: {
|
|
||||||
backgroundColor: 'rgba(22, 33, 62, 0.6)',
|
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
position: 'absolute',
|
|
||||||
top: 50,
|
|
||||||
left: spacing.md,
|
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
},
|
},
|
||||||
backButtonInner: {
|
backButtonInner: {
|
||||||
width: 40,
|
width: 36,
|
||||||
height: 40,
|
height: 36,
|
||||||
borderRadius: 20,
|
borderRadius: 18,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.glass.border,
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
navBarTitle: {
|
||||||
|
flex: 1,
|
||||||
|
...typography.headline,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: "center",
|
||||||
|
marginRight: 36,
|
||||||
|
},
|
||||||
|
navBarSpacer: {
|
||||||
|
width: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hero Section
|
||||||
|
heroContainer: {
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
heroInner: {
|
||||||
|
paddingTop: STATUS_BAR_HEIGHT + NAV_BAR_HEIGHT + spacing.md,
|
||||||
|
paddingBottom: spacing.lg,
|
||||||
|
minHeight: HERO_HEIGHT,
|
||||||
},
|
},
|
||||||
heroContent: {
|
heroContent: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingTop: spacing.xl,
|
},
|
||||||
|
|
||||||
|
// Premium Banner
|
||||||
|
premiumBanner: {
|
||||||
|
marginHorizontal: spacing.lg,
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
borderRadius: radius.md,
|
||||||
|
overflow: 'hidden',
|
||||||
|
shadowColor: colors.premium.gold,
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 8,
|
||||||
|
},
|
||||||
|
premiumBannerContent: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
premiumBannerText: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontWeight: '800',
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: colors.background,
|
||||||
|
},
|
||||||
|
|
||||||
|
label: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: ACCENT_COLOR,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
labelPremium: {
|
||||||
|
color: colors.premium.gold,
|
||||||
|
},
|
||||||
|
iconWrapper: {
|
||||||
|
position: 'relative',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
iconGlowOuter: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -20,
|
||||||
|
left: -20,
|
||||||
|
right: -20,
|
||||||
|
bottom: -20,
|
||||||
|
borderRadius: 60,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.08)',
|
||||||
|
},
|
||||||
|
iconGlow: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -10,
|
||||||
|
left: -10,
|
||||||
|
right: -10,
|
||||||
|
bottom: -10,
|
||||||
|
borderRadius: 50,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
},
|
},
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
width: 80,
|
width: 72,
|
||||||
height: 80,
|
height: 72,
|
||||||
borderRadius: 40,
|
borderRadius: 36,
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
backgroundColor: 'rgba(255, 255, 255, 0.06)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: spacing.md,
|
borderWidth: 1,
|
||||||
borderWidth: 2,
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
borderColor: 'rgba(255, 255, 255, 0.15)',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
iconContainerPremium: {
|
iconContainerPremium: {
|
||||||
borderColor: colors.premium.gold,
|
borderColor: colors.premium.gold,
|
||||||
backgroundColor: colors.premium.goldLight,
|
borderWidth: 2,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.15)',
|
||||||
|
shadowColor: colors.premium.gold,
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
shadowOpacity: 0.5,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 8,
|
||||||
|
},
|
||||||
|
crownBadge: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -4,
|
||||||
|
right: -4,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: colors.premium.gold,
|
||||||
|
shadowColor: colors.premium.gold,
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.4,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 4,
|
||||||
},
|
},
|
||||||
emoji: {
|
emoji: {
|
||||||
fontSize: 40,
|
fontSize: 36,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
...typography.largeTitle,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
titlePremium: {
|
||||||
|
textShadowColor: 'rgba(212, 175, 55, 0.3)',
|
||||||
|
textShadowOffset: { width: 0, height: 0 },
|
||||||
|
textShadowRadius: 10,
|
||||||
},
|
},
|
||||||
proBadge: {
|
proBadge: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.premium.gold,
|
paddingHorizontal: spacing.md,
|
||||||
paddingHorizontal: spacing.sm,
|
paddingVertical: spacing.xs,
|
||||||
paddingVertical: 4,
|
borderRadius: radius.full,
|
||||||
borderRadius: radius.sm,
|
gap: spacing.xs,
|
||||||
gap: 4,
|
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
|
overflow: 'hidden',
|
||||||
|
shadowColor: colors.premium.gold,
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.4,
|
||||||
|
shadowRadius: 6,
|
||||||
|
elevation: 6,
|
||||||
},
|
},
|
||||||
proBadgeText: {
|
proBadgeText: {
|
||||||
...typography.caption1,
|
...typography.caption1,
|
||||||
color: colors.text,
|
color: colors.background,
|
||||||
fontWeight: '700',
|
fontWeight: '800',
|
||||||
|
letterSpacing: 1.5,
|
||||||
},
|
},
|
||||||
breadcrumb: {
|
separator: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
justifyContent: 'center',
|
||||||
marginBottom: spacing.xs,
|
marginVertical: spacing.md,
|
||||||
|
width: 140,
|
||||||
},
|
},
|
||||||
breadcrumbText: {
|
separatorLine: {
|
||||||
...typography.caption1,
|
flex: 1,
|
||||||
color: colors.textSecondary,
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(233, 69, 96, 0.25)',
|
||||||
},
|
},
|
||||||
title: {
|
separatorLinePremium: {
|
||||||
...typography.title2,
|
backgroundColor: 'rgba(212, 175, 55, 0.4)',
|
||||||
color: colors.text,
|
},
|
||||||
textAlign: 'center',
|
separatorDiamond: {
|
||||||
marginBottom: spacing.xs,
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: ACCENT_COLOR,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
separatorDiamondPremium: {
|
||||||
|
backgroundColor: colors.premium.gold,
|
||||||
|
opacity: 1,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
...typography.body,
|
...typography.body,
|
||||||
@@ -438,37 +643,57 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
|
marginBottom: spacing.md,
|
||||||
},
|
},
|
||||||
statsBadge: {
|
statsBadge: {
|
||||||
marginTop: spacing.md,
|
flexDirection: 'row',
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.md,
|
gap: spacing.xs,
|
||||||
paddingVertical: spacing.xs,
|
backgroundColor: 'rgba(233, 69, 96, 0.1)',
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
borderRadius: radius.full,
|
borderRadius: radius.full,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.glass.border,
|
borderColor: 'rgba(233, 69, 96, 0.2)',
|
||||||
|
},
|
||||||
|
statsBadgePremium: {
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.3)',
|
||||||
},
|
},
|
||||||
statsText: {
|
statsText: {
|
||||||
...typography.subhead,
|
...typography.subhead,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
statsTextPremium: {
|
||||||
|
color: colors.premium.champagne,
|
||||||
|
},
|
||||||
|
|
||||||
|
// List
|
||||||
list: {
|
list: {
|
||||||
padding: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingBottom: Platform.OS === 'ios' ? 100 : 80,
|
paddingBottom: Platform.OS === 'ios' ? 100 : 80,
|
||||||
},
|
},
|
||||||
questionCard: {
|
questionCard: {
|
||||||
marginBottom: spacing.md,
|
backgroundColor: 'rgba(22, 33, 62, 0.4)',
|
||||||
overflow: 'hidden',
|
borderRadius: radius.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.06)',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
},
|
},
|
||||||
questionContent: {
|
questionContent: {
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
},
|
},
|
||||||
|
questionNumber: {
|
||||||
|
...typography.caption2,
|
||||||
|
color: colors.textTertiary,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
questionText: {
|
questionText: {
|
||||||
...typography.body,
|
...typography.body,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
lineHeight: 24,
|
lineHeight: 22,
|
||||||
},
|
},
|
||||||
partnerTextContainer: {
|
partnerTextContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -477,7 +702,7 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.xs,
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
backgroundColor: colors.premium.roseLight,
|
backgroundColor: 'rgba(232, 164, 174, 0.1)',
|
||||||
borderRadius: radius.sm,
|
borderRadius: radius.sm,
|
||||||
alignSelf: 'flex-start',
|
alignSelf: 'flex-start',
|
||||||
},
|
},
|
||||||
@@ -491,23 +716,22 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
|
paddingTop: spacing.xs,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: 'rgba(255, 255, 255, 0.04)',
|
||||||
},
|
},
|
||||||
intensityContainer: {
|
intensityContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
badgesContainer: {
|
badgesContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: spacing.sm,
|
gap: spacing.xs,
|
||||||
},
|
},
|
||||||
targetBadge: {
|
targetBadge: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
paddingHorizontal: spacing.xs,
|
||||||
paddingHorizontal: spacing.sm,
|
paddingVertical: 2,
|
||||||
paddingVertical: spacing.xs,
|
|
||||||
borderRadius: radius.sm,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.glass.border,
|
|
||||||
},
|
},
|
||||||
coupleIconGroup: {
|
coupleIconGroup: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- AppAuth (2.0.0):
|
||||||
|
- AppAuth/Core (= 2.0.0)
|
||||||
|
- AppAuth/ExternalUserAgent (= 2.0.0)
|
||||||
|
- AppAuth/Core (2.0.0)
|
||||||
|
- AppAuth/ExternalUserAgent (2.0.0):
|
||||||
|
- AppAuth/Core
|
||||||
|
- AppCheckCore (11.2.0):
|
||||||
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
- boost (1.84.0)
|
- boost (1.84.0)
|
||||||
- DoubleConversion (1.1.6)
|
- DoubleConversion (1.1.6)
|
||||||
- EXApplication (6.0.2):
|
- EXApplication (6.0.2):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
- EXAV (15.0.2):
|
||||||
|
- ExpoModulesCore
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
- EXConstants (17.0.8):
|
- EXConstants (17.0.8):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXImageLoader (5.0.0):
|
- EXImageLoader (5.0.0):
|
||||||
@@ -222,6 +235,10 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
|
- ExpoAdapterGoogleSignIn (16.1.1):
|
||||||
|
- ExpoModulesCore
|
||||||
|
- GoogleSignIn (~> 9.0)
|
||||||
|
- React-Core
|
||||||
- ExpoAppleAuthentication (7.1.3):
|
- ExpoAppleAuthentication (7.1.3):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoAsset (11.0.5):
|
- ExpoAsset (11.0.5):
|
||||||
@@ -285,6 +302,24 @@ PODS:
|
|||||||
- FBLazyVector (0.76.9)
|
- FBLazyVector (0.76.9)
|
||||||
- fmt (11.0.2)
|
- fmt (11.0.2)
|
||||||
- glog (0.3.5)
|
- glog (0.3.5)
|
||||||
|
- GoogleSignIn (9.0.0):
|
||||||
|
- AppAuth (~> 2.0)
|
||||||
|
- AppCheckCore (~> 11.0)
|
||||||
|
- GTMAppAuth (~> 5.0)
|
||||||
|
- GTMSessionFetcher/Core (~> 3.3)
|
||||||
|
- GoogleUtilities/Environment (8.1.0):
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Logger (8.1.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Privacy (8.1.0)
|
||||||
|
- GoogleUtilities/UserDefaults (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GTMAppAuth (5.0.0):
|
||||||
|
- AppAuth/Core (~> 2.0)
|
||||||
|
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
||||||
|
- GTMSessionFetcher/Core (3.5.0)
|
||||||
- hermes-engine (0.76.9):
|
- hermes-engine (0.76.9):
|
||||||
- hermes-engine/Pre-built (= 0.76.9)
|
- hermes-engine/Pre-built (= 0.76.9)
|
||||||
- hermes-engine/Pre-built (0.76.9)
|
- hermes-engine/Pre-built (0.76.9)
|
||||||
@@ -293,6 +328,7 @@ PODS:
|
|||||||
- libavif/core
|
- libavif/core
|
||||||
- libdav1d (>= 0.6.0)
|
- libdav1d (>= 0.6.0)
|
||||||
- libdav1d (1.2.0)
|
- libdav1d (1.2.0)
|
||||||
|
- PromisesObjC (2.4.0)
|
||||||
- PurchasesHybridCommon (17.25.0):
|
- PurchasesHybridCommon (17.25.0):
|
||||||
- RevenueCat (= 5.51.1)
|
- RevenueCat (= 5.51.1)
|
||||||
- RCT-Folly (2024.10.14.00):
|
- RCT-Folly (2024.10.14.00):
|
||||||
@@ -1573,6 +1609,27 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
|
- react-native-compressor (1.13.0):
|
||||||
|
- DoubleConversion
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly (= 2024.10.14.00)
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-ImageManager
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- Yoga
|
||||||
- react-native-safe-area-context (4.12.0):
|
- react-native-safe-area-context (4.12.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-nativeconfig (0.76.9)
|
- React-nativeconfig (0.76.9)
|
||||||
@@ -1871,6 +1928,9 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
|
- RNGoogleSignin (16.1.1):
|
||||||
|
- GoogleSignIn (~> 9.0)
|
||||||
|
- React-Core
|
||||||
- RNPurchases (9.6.12):
|
- RNPurchases (9.6.12):
|
||||||
- PurchasesHybridCommon (= 17.25.0)
|
- PurchasesHybridCommon (= 17.25.0)
|
||||||
- React-Core
|
- React-Core
|
||||||
@@ -2022,6 +2082,7 @@ DEPENDENCIES:
|
|||||||
- boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
- boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||||
- DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
- DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||||
- EXApplication (from `../../../node_modules/expo-application/ios`)
|
- EXApplication (from `../../../node_modules/expo-application/ios`)
|
||||||
|
- EXAV (from `../../../node_modules/expo-av/ios`)
|
||||||
- EXConstants (from `../../../node_modules/expo-constants/ios`)
|
- EXConstants (from `../../../node_modules/expo-constants/ios`)
|
||||||
- EXImageLoader (from `../../../node_modules/expo-image-loader/ios`)
|
- EXImageLoader (from `../../../node_modules/expo-image-loader/ios`)
|
||||||
- EXJSONUtils (from `../../../node_modules/expo-json-utils/ios`)
|
- EXJSONUtils (from `../../../node_modules/expo-json-utils/ios`)
|
||||||
@@ -2032,6 +2093,7 @@ DEPENDENCIES:
|
|||||||
- expo-dev-launcher (from `../../../node_modules/expo-dev-launcher`)
|
- expo-dev-launcher (from `../../../node_modules/expo-dev-launcher`)
|
||||||
- expo-dev-menu (from `../../../node_modules/expo-dev-menu`)
|
- expo-dev-menu (from `../../../node_modules/expo-dev-menu`)
|
||||||
- expo-dev-menu-interface (from `../../../node_modules/expo-dev-menu-interface/ios`)
|
- expo-dev-menu-interface (from `../../../node_modules/expo-dev-menu-interface/ios`)
|
||||||
|
- "ExpoAdapterGoogleSignIn (from `../../../node_modules/@react-native-google-signin/google-signin/expo/ios`)"
|
||||||
- ExpoAppleAuthentication (from `../../../node_modules/expo-apple-authentication/ios`)
|
- ExpoAppleAuthentication (from `../../../node_modules/expo-apple-authentication/ios`)
|
||||||
- ExpoAsset (from `../../../node_modules/expo-asset/ios`)
|
- ExpoAsset (from `../../../node_modules/expo-asset/ios`)
|
||||||
- ExpoBlur (from `../../../node_modules/expo-blur/ios`)
|
- ExpoBlur (from `../../../node_modules/expo-blur/ios`)
|
||||||
@@ -2085,6 +2147,7 @@ DEPENDENCIES:
|
|||||||
- React-logger (from `../../../node_modules/react-native/ReactCommon/logger`)
|
- React-logger (from `../../../node_modules/react-native/ReactCommon/logger`)
|
||||||
- React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`)
|
- React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||||
|
- react-native-compressor (from `../../../node_modules/react-native-compressor`)
|
||||||
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
|
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
|
||||||
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
|
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||||
@@ -2115,6 +2178,7 @@ DEPENDENCIES:
|
|||||||
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
|
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
|
- "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
- RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`)
|
- RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`)
|
||||||
|
- "RNGoogleSignin (from `../../../node_modules/@react-native-google-signin/google-signin`)"
|
||||||
- RNPurchases (from `../../../node_modules/react-native-purchases`)
|
- RNPurchases (from `../../../node_modules/react-native-purchases`)
|
||||||
- RNReanimated (from `../../../node_modules/react-native-reanimated`)
|
- RNReanimated (from `../../../node_modules/react-native-reanimated`)
|
||||||
- RNScreens (from `../../../node_modules/react-native-screens`)
|
- RNScreens (from `../../../node_modules/react-native-screens`)
|
||||||
@@ -2123,8 +2187,15 @@ DEPENDENCIES:
|
|||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
|
- AppAuth
|
||||||
|
- AppCheckCore
|
||||||
|
- GoogleSignIn
|
||||||
|
- GoogleUtilities
|
||||||
|
- GTMAppAuth
|
||||||
|
- GTMSessionFetcher
|
||||||
- libavif
|
- libavif
|
||||||
- libdav1d
|
- libdav1d
|
||||||
|
- PromisesObjC
|
||||||
- PurchasesHybridCommon
|
- PurchasesHybridCommon
|
||||||
- RevenueCat
|
- RevenueCat
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
@@ -2140,6 +2211,8 @@ EXTERNAL SOURCES:
|
|||||||
:podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
:podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||||
EXApplication:
|
EXApplication:
|
||||||
:path: "../../../node_modules/expo-application/ios"
|
:path: "../../../node_modules/expo-application/ios"
|
||||||
|
EXAV:
|
||||||
|
:path: "../../../node_modules/expo-av/ios"
|
||||||
EXConstants:
|
EXConstants:
|
||||||
:path: "../../../node_modules/expo-constants/ios"
|
:path: "../../../node_modules/expo-constants/ios"
|
||||||
EXImageLoader:
|
EXImageLoader:
|
||||||
@@ -2160,6 +2233,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../../node_modules/expo-dev-menu"
|
:path: "../../../node_modules/expo-dev-menu"
|
||||||
expo-dev-menu-interface:
|
expo-dev-menu-interface:
|
||||||
:path: "../../../node_modules/expo-dev-menu-interface/ios"
|
:path: "../../../node_modules/expo-dev-menu-interface/ios"
|
||||||
|
ExpoAdapterGoogleSignIn:
|
||||||
|
:path: "../../../node_modules/@react-native-google-signin/google-signin/expo/ios"
|
||||||
ExpoAppleAuthentication:
|
ExpoAppleAuthentication:
|
||||||
:path: "../../../node_modules/expo-apple-authentication/ios"
|
:path: "../../../node_modules/expo-apple-authentication/ios"
|
||||||
ExpoAsset:
|
ExpoAsset:
|
||||||
@@ -2263,6 +2338,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../../node_modules/react-native/ReactCommon"
|
:path: "../../../node_modules/react-native/ReactCommon"
|
||||||
React-microtasksnativemodule:
|
React-microtasksnativemodule:
|
||||||
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||||
|
react-native-compressor:
|
||||||
|
:path: "../../../node_modules/react-native-compressor"
|
||||||
react-native-safe-area-context:
|
react-native-safe-area-context:
|
||||||
:path: "../../../node_modules/react-native-safe-area-context"
|
:path: "../../../node_modules/react-native-safe-area-context"
|
||||||
React-nativeconfig:
|
React-nativeconfig:
|
||||||
@@ -2323,6 +2400,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../../node_modules/@react-native-async-storage/async-storage"
|
:path: "../../../node_modules/@react-native-async-storage/async-storage"
|
||||||
RNGestureHandler:
|
RNGestureHandler:
|
||||||
:path: "../../../node_modules/react-native-gesture-handler"
|
:path: "../../../node_modules/react-native-gesture-handler"
|
||||||
|
RNGoogleSignin:
|
||||||
|
:path: "../../../node_modules/@react-native-google-signin/google-signin"
|
||||||
RNPurchases:
|
RNPurchases:
|
||||||
:path: "../../../node_modules/react-native-purchases"
|
:path: "../../../node_modules/react-native-purchases"
|
||||||
RNReanimated:
|
RNReanimated:
|
||||||
@@ -2335,9 +2414,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../../node_modules/react-native/ReactCommon/yoga"
|
:path: "../../../node_modules/react-native/ReactCommon/yoga"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
||||||
|
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
||||||
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
|
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
|
||||||
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
||||||
EXApplication: 4c72f6017a14a65e338c5e74fca418f35141e819
|
EXApplication: 4c72f6017a14a65e338c5e74fca418f35141e819
|
||||||
|
EXAV: 4c41f0a6ae54a0b96597abc90e37962ab2ee0d85
|
||||||
EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b
|
EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b
|
||||||
EXImageLoader: e5da974e25b13585c196b658a440720c075482d5
|
EXImageLoader: e5da974e25b13585c196b658a440720c075482d5
|
||||||
EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93
|
EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93
|
||||||
@@ -2348,6 +2430,7 @@ SPEC CHECKSUMS:
|
|||||||
expo-dev-launcher: 82de1bf29e91f4794bcc65a697e7066d9a826bee
|
expo-dev-launcher: 82de1bf29e91f4794bcc65a697e7066d9a826bee
|
||||||
expo-dev-menu: 3fda3eb2b3394dc92955f6ff60cb278f843641be
|
expo-dev-menu: 3fda3eb2b3394dc92955f6ff60cb278f843641be
|
||||||
expo-dev-menu-interface: 00dc42302a72722fdecec3fa048de84a9133bcc4
|
expo-dev-menu-interface: 00dc42302a72722fdecec3fa048de84a9133bcc4
|
||||||
|
ExpoAdapterGoogleSignIn: 21ff1daeef3d21cb0b45772d8029102cc728d6b1
|
||||||
ExpoAppleAuthentication: 52631ed9dcb71c65712a447bbb9a5667bb8fcf0c
|
ExpoAppleAuthentication: 52631ed9dcb71c65712a447bbb9a5667bb8fcf0c
|
||||||
ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516
|
ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516
|
||||||
ExpoBlur: 392c1207f71d0ecf22371621c1fbd44ba84d9742
|
ExpoBlur: 392c1207f71d0ecf22371621c1fbd44ba84d9742
|
||||||
@@ -2369,9 +2452,14 @@ SPEC CHECKSUMS:
|
|||||||
FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45
|
FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45
|
||||||
fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6
|
fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6
|
||||||
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
|
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
|
||||||
|
GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77
|
||||||
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
|
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
||||||
|
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||||
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
|
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
|
||||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||||
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PurchasesHybridCommon: 6a79a873ab52f777bfa36e9516f3fcd84d3b3428
|
PurchasesHybridCommon: 6a79a873ab52f777bfa36e9516f3fcd84d3b3428
|
||||||
RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17
|
RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17
|
||||||
RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83
|
RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83
|
||||||
@@ -2402,6 +2490,7 @@ SPEC CHECKSUMS:
|
|||||||
React-logger: c4052eb941cca9a097ef01b59543a656dc088559
|
React-logger: c4052eb941cca9a097ef01b59543a656dc088559
|
||||||
React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de
|
React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de
|
||||||
React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3
|
React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3
|
||||||
|
react-native-compressor: 2c3a41768bdd367f6e121f555dff9d3eda1810e5
|
||||||
react-native-safe-area-context: 8b8404e70b0cbf2a56428a17017c14c1dcc16448
|
react-native-safe-area-context: 8b8404e70b0cbf2a56428a17017c14c1dcc16448
|
||||||
React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678
|
React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678
|
||||||
React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e
|
React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e
|
||||||
@@ -2433,6 +2522,7 @@ SPEC CHECKSUMS:
|
|||||||
RevenueCat: eab035bbab271faccfef5c36eaff2a1ffef14dc0
|
RevenueCat: eab035bbab271faccfef5c36eaff2a1ffef14dc0
|
||||||
RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11
|
RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11
|
||||||
RNGestureHandler: 32336458ef842088a459ccfcc10b583186de2633
|
RNGestureHandler: 32336458ef842088a459ccfcc10b583186de2633
|
||||||
|
RNGoogleSignin: 5782dd7dad559608a06f3c6b1d4142c395fb5308
|
||||||
RNPurchases: cc3333cede6c0f040545a95c38ae627d0174c058
|
RNPurchases: cc3333cede6c0f040545a95c38ae627d0174c058
|
||||||
RNReanimated: 82bf292e3c590d2d9886039f83e1baf0c1b2cdc4
|
RNReanimated: 82bf292e3c590d2d9886039f83e1baf0c1b2cdc4
|
||||||
RNScreens: 295d9c0aaeb7f680d03d7e9b476569a4959aae89
|
RNScreens: 295d9c0aaeb7f680d03d7e9b476569a4959aae89
|
||||||
|
|||||||
@@ -174,8 +174,8 @@
|
|||||||
LastUpgradeCheck = 1130;
|
LastUpgradeCheck = 1130;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
13B07F861A680F5B00A75B9A = {
|
13B07F861A680F5B00A75B9A = {
|
||||||
|
DevelopmentTeam = BXVPBATDZY;
|
||||||
LastSwiftMigration = 1250;
|
LastSwiftMigration = 1250;
|
||||||
DevelopmentTeam = "BXVPBATDZY";
|
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -271,11 +271,17 @@
|
|||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh",
|
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/AppAuth/AppAuthCore_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/GTMAppAuth/GTMAppAuth_Privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||||
@@ -291,11 +297,17 @@
|
|||||||
);
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AppAuthCore_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMAppAuth_Privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||||
@@ -375,7 +387,10 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -398,9 +413,6 @@
|
|||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
DEVELOPMENT_TEAM = "BXVPBATDZY";
|
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -411,7 +423,10 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||||
INFOPLIST_FILE = Sauci/Info.plist;
|
INFOPLIST_FILE = Sauci/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
@@ -428,9 +443,6 @@
|
|||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
DEVELOPMENT_TEAM = "BXVPBATDZY";
|
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>CA92.1</string>
|
<string>CA92.1</string>
|
||||||
|
<string>C56D.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-apple-authentication": "~7.1.3",
|
"expo-apple-authentication": "~7.1.3",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
|
"expo-av": "~15.0.2",
|
||||||
"expo-blur": "~14.0.3",
|
"expo-blur": "~14.0.3",
|
||||||
"expo-build-properties": "^0.13.3",
|
"expo-build-properties": "^0.13.3",
|
||||||
"expo-clipboard": "~7.0.1",
|
"expo-clipboard": "~7.0.1",
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
|
"react-native-compressor": "^1.13.0",
|
||||||
"react-native-gesture-handler": "~2.20.0",
|
"react-native-gesture-handler": "~2.20.0",
|
||||||
"react-native-purchases": "^9.6.12",
|
"react-native-purchases": "^9.6.12",
|
||||||
"react-native-reanimated": "~3.16.0",
|
"react-native-reanimated": "~3.16.0",
|
||||||
|
|||||||
@@ -121,6 +121,19 @@ export function PackTeaser({
|
|||||||
<Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
|
<Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<Animated.View style={[styles.content, contentStyle]}>
|
<Animated.View style={[styles.content, contentStyle]}>
|
||||||
|
{/* Premium gradient background */}
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(22, 33, 62, 0.98)', 'rgba(13, 13, 26, 1)']}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
{/* Top silk highlight */}
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(212, 175, 55, 0.08)', 'transparent']}
|
||||||
|
style={styles.silkHighlight}
|
||||||
|
/>
|
||||||
|
{/* Premium border accent */}
|
||||||
|
<View style={styles.premiumBorderAccent} />
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
@@ -136,12 +149,35 @@ export function PackTeaser({
|
|||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
|
{/* Premium label */}
|
||||||
|
<Text style={styles.label}>PREMIUM COLLECTION</Text>
|
||||||
|
|
||||||
|
{/* Icon with glow */}
|
||||||
|
<View style={styles.iconWrapper}>
|
||||||
<View style={styles.iconContainer}>
|
<View style={styles.iconContainer}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(212, 175, 55, 0.15)', 'rgba(184, 134, 11, 0.1)']}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
<Text style={styles.packEmoji}>{packIcon || "📦"}</Text>
|
<Text style={styles.packEmoji}>{packIcon || "📦"}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{/* Crown badge */}
|
||||||
|
<View style={styles.crownBadge}>
|
||||||
|
<Ionicons name="star" size={12} color={colors.premium.gold} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={styles.title}>{packName}</Text>
|
<Text style={styles.title}>{packName}</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
Preview a few questions from this pack
|
Preview a few questions from this exclusive pack
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -149,7 +185,7 @@ export function PackTeaser({
|
|||||||
<View style={styles.questionsContainer}>
|
<View style={styles.questionsContainer}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator color={colors.primary} size="large" />
|
<ActivityIndicator color={colors.premium.gold} size="large" />
|
||||||
<Text style={styles.loadingText}>
|
<Text style={styles.loadingText}>
|
||||||
Loading preview...
|
Loading preview...
|
||||||
</Text>
|
</Text>
|
||||||
@@ -159,12 +195,12 @@ export function PackTeaser({
|
|||||||
<View key={question.id} style={styles.questionCard}>
|
<View key={question.id} style={styles.questionCard}>
|
||||||
<View style={styles.questionHeader}>
|
<View style={styles.questionHeader}>
|
||||||
<View style={styles.intensityContainer}>
|
<View style={styles.intensityContainer}>
|
||||||
{[...Array(question.intensity)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
key={i}
|
key={i}
|
||||||
name="flame"
|
name={i < question.intensity ? "flame" : "flame-outline"}
|
||||||
size={12}
|
size={12}
|
||||||
color={colors.primary}
|
color={i < question.intensity ? colors.premium.gold : 'rgba(255, 255, 255, 0.2)'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -188,40 +224,39 @@ export function PackTeaser({
|
|||||||
{questions.length > 0 && (
|
{questions.length > 0 && (
|
||||||
<View style={styles.moreHint}>
|
<View style={styles.moreHint}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="lock-closed"
|
name="diamond"
|
||||||
size={16}
|
size={16}
|
||||||
color={colors.textTertiary}
|
color={colors.premium.gold}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.moreHintText}>
|
<Text style={styles.moreHintText}>
|
||||||
+ many more questions to unlock
|
+ many more exclusive questions to unlock
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Unlock Button */}
|
{/* Unlock Button */}
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<TouchableOpacity
|
<Pressable
|
||||||
style={styles.unlockButton}
|
style={styles.unlockButton}
|
||||||
onPress={onUnlock}
|
onPress={onUnlock}
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||||
style={styles.unlockButtonGradient}
|
style={styles.unlockButtonGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 0 }}
|
end={{ x: 1, y: 0 }}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="star"
|
name="diamond"
|
||||||
size={20}
|
size={18}
|
||||||
color={colors.text}
|
color={colors.background}
|
||||||
style={{ marginRight: spacing.sm }}
|
style={{ marginRight: spacing.sm }}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.unlockButtonText}>
|
<Text style={styles.unlockButtonText}>
|
||||||
Unlock with Pro
|
Unlock with Pro
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -237,16 +272,35 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
backgroundColor: "rgba(0, 0, 0, 0.85)",
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
backgroundColor: colors.backgroundLight,
|
backgroundColor: colors.background,
|
||||||
borderTopLeftRadius: radius.xxl,
|
borderTopLeftRadius: radius.xxl,
|
||||||
borderTopRightRadius: radius.xxl,
|
borderTopRightRadius: radius.xxl,
|
||||||
maxHeight: "85%",
|
maxHeight: "85%",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderBottomWidth: 0,
|
borderBottomWidth: 0,
|
||||||
borderColor: colors.glass.border,
|
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
silkHighlight: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 150,
|
||||||
|
borderTopLeftRadius: radius.xxl,
|
||||||
|
borderTopRightRadius: radius.xxl,
|
||||||
|
},
|
||||||
|
premiumBorderAccent: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 2,
|
||||||
|
backgroundColor: colors.premium.gold,
|
||||||
|
opacity: 0.4,
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
padding: spacing.lg,
|
padding: spacing.lg,
|
||||||
@@ -260,36 +314,98 @@ const styles = StyleSheet.create({
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: colors.glass.background,
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: spacing.xl,
|
marginBottom: spacing.xl,
|
||||||
},
|
},
|
||||||
iconContainer: {
|
label: {
|
||||||
width: 72,
|
...typography.caption1,
|
||||||
height: 72,
|
fontWeight: '600',
|
||||||
borderRadius: 36,
|
letterSpacing: 3,
|
||||||
backgroundColor: colors.glass.background,
|
color: colors.premium.gold,
|
||||||
justifyContent: "center",
|
textAlign: 'center',
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
},
|
},
|
||||||
|
iconWrapper: {
|
||||||
|
position: 'relative',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 40,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: colors.premium.gold,
|
||||||
|
shadowColor: colors.premium.gold,
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
shadowOpacity: 0.6,
|
||||||
|
shadowRadius: 20,
|
||||||
|
elevation: 12,
|
||||||
|
},
|
||||||
|
crownBadge: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -4,
|
||||||
|
right: -4,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: colors.premium.gold,
|
||||||
|
shadowColor: colors.premium.gold,
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.4,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 4,
|
||||||
|
},
|
||||||
packEmoji: {
|
packEmoji: {
|
||||||
fontSize: 36,
|
fontSize: 40,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
...typography.title1,
|
...typography.title1,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: spacing.xs,
|
marginBottom: spacing.xs,
|
||||||
|
textShadowColor: 'rgba(212, 175, 55, 0.3)',
|
||||||
|
textShadowOffset: { width: 0, height: 0 },
|
||||||
|
textShadowRadius: 10,
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
separatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.3)',
|
||||||
|
},
|
||||||
|
separatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: colors.premium.gold,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
...typography.body,
|
...typography.body,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
|
lineHeight: 24,
|
||||||
},
|
},
|
||||||
questionsContainer: {
|
questionsContainer: {
|
||||||
marginBottom: spacing.lg,
|
marginBottom: spacing.lg,
|
||||||
@@ -305,10 +421,10 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: spacing.md,
|
marginTop: spacing.md,
|
||||||
},
|
},
|
||||||
questionCard: {
|
questionCard: {
|
||||||
backgroundColor: colors.glass.background,
|
backgroundColor: 'rgba(22, 33, 62, 0.4)',
|
||||||
borderRadius: radius.lg,
|
borderRadius: radius.lg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.glass.border,
|
borderColor: 'rgba(212, 175, 55, 0.15)',
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
},
|
},
|
||||||
questionHeader: {
|
questionHeader: {
|
||||||
@@ -319,20 +435,23 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
intensityContainer: {
|
intensityContainer: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
backgroundColor: colors.primaryLight,
|
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
borderRadius: radius.full,
|
borderRadius: radius.full,
|
||||||
gap: 1,
|
gap: 2,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.15)',
|
||||||
},
|
},
|
||||||
questionNumber: {
|
questionNumber: {
|
||||||
...typography.caption2,
|
...typography.caption2,
|
||||||
color: colors.textTertiary,
|
color: colors.premium.champagne,
|
||||||
|
opacity: 0.7,
|
||||||
},
|
},
|
||||||
questionText: {
|
questionText: {
|
||||||
...typography.body,
|
...typography.body,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
lineHeight: 22,
|
lineHeight: 24,
|
||||||
},
|
},
|
||||||
emptyContainer: {
|
emptyContainer: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -347,11 +466,19 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: spacing.lg,
|
marginBottom: spacing.lg,
|
||||||
gap: spacing.xs,
|
gap: spacing.sm,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.08)',
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
alignSelf: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.15)',
|
||||||
},
|
},
|
||||||
moreHintText: {
|
moreHintText: {
|
||||||
...typography.caption1,
|
...typography.caption1,
|
||||||
color: colors.textTertiary,
|
color: colors.premium.champagne,
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
@@ -359,6 +486,11 @@ const styles = StyleSheet.create({
|
|||||||
unlockButton: {
|
unlockButton: {
|
||||||
borderRadius: radius.lg,
|
borderRadius: radius.lg,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
shadowColor: colors.premium.gold,
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 8,
|
||||||
},
|
},
|
||||||
unlockButtonGradient: {
|
unlockButtonGradient: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -368,7 +500,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
unlockButtonText: {
|
unlockButtonText: {
|
||||||
...typography.headline,
|
...typography.headline,
|
||||||
color: colors.text,
|
color: colors.background,
|
||||||
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Platform,
|
Platform,
|
||||||
|
Pressable,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
@@ -24,9 +25,9 @@ interface PaywallProps {
|
|||||||
|
|
||||||
const FEATURES = [
|
const FEATURES = [
|
||||||
{
|
{
|
||||||
icon: "heart" as const,
|
icon: "diamond" as const,
|
||||||
title: "Unlock All Packs",
|
title: "Unlock All Packs",
|
||||||
description: "Access every question pack",
|
description: "Access every premium collection",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "sparkles" as const,
|
icon: "sparkles" as const,
|
||||||
@@ -34,7 +35,7 @@ const FEATURES = [
|
|||||||
description: "New packs added regularly",
|
description: "New packs added regularly",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "people" as const,
|
icon: "heart" as const,
|
||||||
title: "Share with Partner",
|
title: "Share with Partner",
|
||||||
description: "Both of you get Pro access",
|
description: "Both of you get Pro access",
|
||||||
},
|
},
|
||||||
@@ -125,6 +126,7 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
|
|
||||||
const savings = calculateSavings();
|
const savings = calculateSavings();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@@ -134,6 +136,19 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
>
|
>
|
||||||
<View style={styles.overlay}>
|
<View style={styles.overlay}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
|
{/* Premium gradient background */}
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(22, 33, 62, 0.98)', 'rgba(13, 13, 26, 1)']}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
{/* Top silk highlight */}
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(212, 175, 55, 0.08)', 'transparent']}
|
||||||
|
style={styles.silkHighlight}
|
||||||
|
/>
|
||||||
|
{/* Premium border accent */}
|
||||||
|
<View style={styles.premiumBorderAccent} />
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
@@ -149,15 +164,29 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
|
{/* Premium label */}
|
||||||
|
<Text style={styles.label}>EXCLUSIVE ACCESS</Text>
|
||||||
|
|
||||||
|
{/* Icon with glow */}
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||||
style={styles.iconContainer}
|
style={StyleSheet.absoluteFill}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
>
|
/>
|
||||||
<Ionicons name="star" size={32} color={colors.text} />
|
<Ionicons name="diamond" size={32} color={colors.background} />
|
||||||
</LinearGradient>
|
</View>
|
||||||
<Text style={styles.title}>Upgrade to Pro</Text>
|
|
||||||
|
<Text style={styles.title}>Sauci Pro</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
Unlock all premium content for you and your partner
|
Unlock all premium content for you and your partner
|
||||||
</Text>
|
</Text>
|
||||||
@@ -165,13 +194,13 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<View style={styles.features}>
|
<View style={styles.features}>
|
||||||
{FEATURES.map((feature) => (
|
{FEATURES.map((feature, index) => (
|
||||||
<View key={feature.title} style={styles.featureRow}>
|
<View key={feature.title} style={styles.featureRow}>
|
||||||
<View style={styles.featureIcon}>
|
<View style={styles.featureIcon}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={feature.icon}
|
name={feature.icon}
|
||||||
size={20}
|
size={18}
|
||||||
color={colors.primary}
|
color={colors.premium.gold}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.featureText}>
|
<View style={styles.featureText}>
|
||||||
@@ -180,6 +209,11 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
{feature.description}
|
{feature.description}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<Ionicons
|
||||||
|
name="checkmark-circle"
|
||||||
|
size={20}
|
||||||
|
color={colors.premium.gold}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -188,9 +222,9 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
<View style={styles.packages}>
|
<View style={styles.packages}>
|
||||||
{isLoadingOfferings ? (
|
{isLoadingOfferings ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator color={colors.primary} size="large" />
|
<ActivityIndicator color={colors.premium.gold} size="large" />
|
||||||
<Text style={styles.loadingText}>
|
<Text style={styles.loadingText}>
|
||||||
Loading subscription options...
|
Loading options...
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : offerings?.availablePackages ? (
|
) : offerings?.availablePackages ? (
|
||||||
@@ -200,17 +234,30 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
const isAnnual = pkg.packageType === "ANNUAL";
|
const isAnnual = pkg.packageType === "ANNUAL";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<Pressable
|
||||||
key={pkg.identifier}
|
key={pkg.identifier}
|
||||||
onPress={() => setSelectedPackage(pkg)}
|
onPress={() => setSelectedPackage(pkg)}
|
||||||
activeOpacity={0.7}
|
|
||||||
style={[
|
style={[
|
||||||
styles.packageCard,
|
styles.packageCard,
|
||||||
isSelected && styles.packageCardSelected,
|
isSelected && styles.packageCardSelected,
|
||||||
|
isAnnual && styles.packageCardAnnual,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
{isAnnual && (
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(212, 175, 55, 0.1)', 'rgba(184, 134, 11, 0.05)']}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isAnnual && savings && savings > 0 && (
|
{isAnnual && savings && savings > 0 && (
|
||||||
<View style={styles.savingsBadge}>
|
<View style={styles.savingsBadge}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
/>
|
||||||
|
<Ionicons name="star" size={10} color={colors.background} />
|
||||||
<Text style={styles.savingsText}>
|
<Text style={styles.savingsText}>
|
||||||
SAVE {savings}%
|
SAVE {savings}%
|
||||||
</Text>
|
</Text>
|
||||||
@@ -226,15 +273,28 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
{isSelected && <View style={styles.radioInner} />}
|
{isSelected && <View style={styles.radioInner} />}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.packageInfo}>
|
<View style={styles.packageInfo}>
|
||||||
<Text style={styles.packageTitle}>
|
<View style={styles.packageTitleRow}>
|
||||||
|
<Text style={[
|
||||||
|
styles.packageTitle,
|
||||||
|
isAnnual && styles.packageTitleAnnual
|
||||||
|
]}>
|
||||||
{isAnnual ? "Annual" : "Monthly"}
|
{isAnnual ? "Annual" : "Monthly"}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.packagePrice}>
|
{isAnnual && (
|
||||||
|
<View style={styles.bestValueBadge}>
|
||||||
|
<Text style={styles.bestValueText}>BEST VALUE</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={[
|
||||||
|
styles.packagePrice,
|
||||||
|
isAnnual && styles.packagePriceAnnual
|
||||||
|
]}>
|
||||||
{formatPrice(pkg)}
|
{formatPrice(pkg)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</Pressable>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
@@ -266,7 +326,7 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
|
|
||||||
{/* Purchase Button */}
|
{/* Purchase Button */}
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<TouchableOpacity
|
<Pressable
|
||||||
style={[
|
style={[
|
||||||
styles.purchaseButton,
|
styles.purchaseButton,
|
||||||
(!selectedPackage || isPurchasing) &&
|
(!selectedPackage || isPurchasing) &&
|
||||||
@@ -274,21 +334,23 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
]}
|
]}
|
||||||
onPress={handlePurchase}
|
onPress={handlePurchase}
|
||||||
disabled={!selectedPackage || isPurchasing}
|
disabled={!selectedPackage || isPurchasing}
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
>
|
||||||
{isPurchasing ? (
|
{isPurchasing ? (
|
||||||
<ActivityIndicator size="small" color={colors.text} />
|
<View style={styles.purchaseButtonLoading}>
|
||||||
|
<ActivityIndicator size="small" color={colors.background} />
|
||||||
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={gradients.primary as [string, string]}
|
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||||
style={styles.purchaseButtonGradient}
|
style={styles.purchaseButtonGradient}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 0 }}
|
end={{ x: 1, y: 0 }}
|
||||||
>
|
>
|
||||||
<Text style={styles.purchaseButtonText}>Subscribe Now</Text>
|
<Ionicons name="diamond" size={18} color={colors.background} style={{ marginRight: spacing.sm }} />
|
||||||
|
<Text style={styles.purchaseButtonText}>Unlock Pro</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</Pressable>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.restoreButton}
|
style={styles.restoreButton}
|
||||||
@@ -317,17 +379,36 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
overlay: {
|
overlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
backgroundColor: "rgba(0, 0, 0, 0.85)",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
backgroundColor: colors.backgroundLight,
|
backgroundColor: colors.background,
|
||||||
borderTopLeftRadius: radius.xxl,
|
borderTopLeftRadius: radius.xxl,
|
||||||
borderTopRightRadius: radius.xxl,
|
borderTopRightRadius: radius.xxl,
|
||||||
maxHeight: "90%",
|
maxHeight: "90%",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderBottomWidth: 0,
|
borderBottomWidth: 0,
|
||||||
borderColor: colors.glass.border,
|
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
silkHighlight: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 150,
|
||||||
|
borderTopLeftRadius: radius.xxl,
|
||||||
|
borderTopRightRadius: radius.xxl,
|
||||||
|
},
|
||||||
|
premiumBorderAccent: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 2,
|
||||||
|
backgroundColor: colors.premium.gold,
|
||||||
|
opacity: 0.4,
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
padding: spacing.lg,
|
padding: spacing.lg,
|
||||||
@@ -341,49 +422,96 @@ const styles = StyleSheet.create({
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: colors.glass.background,
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: spacing.xl,
|
marginBottom: spacing.xl,
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: colors.premium.gold,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
width: 72,
|
width: 72,
|
||||||
height: 72,
|
height: 72,
|
||||||
borderRadius: 36,
|
borderRadius: 36,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
overflow: 'hidden',
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
|
shadowColor: colors.premium.gold,
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
shadowOpacity: 0.6,
|
||||||
|
shadowRadius: 20,
|
||||||
|
elevation: 12,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
...typography.title1,
|
...typography.title1,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
marginBottom: spacing.xs,
|
marginBottom: spacing.xs,
|
||||||
|
textShadowColor: 'rgba(212, 175, 55, 0.3)',
|
||||||
|
textShadowOffset: { width: 0, height: 0 },
|
||||||
|
textShadowRadius: 10,
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
separatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.3)',
|
||||||
|
},
|
||||||
|
separatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: colors.premium.gold,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
...typography.body,
|
...typography.body,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
|
lineHeight: 24,
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
marginBottom: spacing.xl,
|
marginBottom: spacing.xl,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.05)',
|
||||||
|
borderRadius: radius.lg,
|
||||||
|
padding: spacing.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.1)',
|
||||||
},
|
},
|
||||||
featureRow: {
|
featureRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: spacing.md,
|
paddingVertical: spacing.sm,
|
||||||
},
|
},
|
||||||
featureIcon: {
|
featureIcon: {
|
||||||
width: 44,
|
width: 40,
|
||||||
height: 44,
|
height: 40,
|
||||||
borderRadius: 22,
|
borderRadius: 20,
|
||||||
backgroundColor: colors.primaryLight,
|
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginRight: spacing.md,
|
marginRight: spacing.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
},
|
},
|
||||||
featureText: {
|
featureText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -411,30 +539,38 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
packageCard: {
|
packageCard: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
backgroundColor: colors.glass.background,
|
backgroundColor: 'rgba(22, 33, 62, 0.4)',
|
||||||
borderRadius: radius.lg,
|
borderRadius: radius.lg,
|
||||||
borderWidth: 2,
|
borderWidth: 1,
|
||||||
borderColor: colors.glass.border,
|
borderColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
overflow: "visible",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
packageCardSelected: {
|
packageCardSelected: {
|
||||||
borderColor: colors.primary,
|
borderColor: colors.premium.gold,
|
||||||
backgroundColor: colors.primaryLight,
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
packageCardAnnual: {
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.3)',
|
||||||
},
|
},
|
||||||
savingsBadge: {
|
savingsBadge: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: -10,
|
top: -1,
|
||||||
right: spacing.md,
|
right: spacing.md,
|
||||||
backgroundColor: colors.primary,
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
borderRadius: radius.xs,
|
borderBottomLeftRadius: radius.sm,
|
||||||
|
borderBottomRightRadius: radius.sm,
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
savingsText: {
|
savingsText: {
|
||||||
...typography.caption2,
|
...typography.caption2,
|
||||||
color: colors.text,
|
color: colors.background,
|
||||||
fontWeight: "700",
|
fontWeight: "800",
|
||||||
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
packageContent: {
|
packageContent: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -445,30 +581,55 @@ const styles = StyleSheet.create({
|
|||||||
height: 24,
|
height: 24,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: colors.textSecondary,
|
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginRight: spacing.md,
|
marginRight: spacing.md,
|
||||||
},
|
},
|
||||||
radioOuterSelected: {
|
radioOuterSelected: {
|
||||||
borderColor: colors.primary,
|
borderColor: colors.premium.gold,
|
||||||
},
|
},
|
||||||
radioInner: {
|
radioInner: {
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.premium.gold,
|
||||||
},
|
},
|
||||||
packageInfo: {
|
packageInfo: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
packageTitleRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
packageTitle: {
|
packageTitle: {
|
||||||
...typography.headline,
|
...typography.headline,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
|
packageTitleAnnual: {
|
||||||
|
color: colors.premium.champagne,
|
||||||
|
},
|
||||||
|
bestValueBadge: {
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
paddingHorizontal: spacing.xs,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: radius.xs,
|
||||||
|
},
|
||||||
|
bestValueText: {
|
||||||
|
...typography.caption2,
|
||||||
|
color: colors.premium.gold,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
packagePrice: {
|
packagePrice: {
|
||||||
...typography.subhead,
|
...typography.subhead,
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
packagePriceAnnual: {
|
||||||
|
color: colors.premium.champagne,
|
||||||
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
errorContainer: {
|
errorContainer: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -481,16 +642,16 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
},
|
},
|
||||||
retryButton: {
|
retryButton: {
|
||||||
backgroundColor: colors.glass.background,
|
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
borderRadius: radius.md,
|
borderRadius: radius.md,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.glass.border,
|
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
},
|
},
|
||||||
retryButtonText: {
|
retryButtonText: {
|
||||||
...typography.subhead,
|
...typography.subhead,
|
||||||
color: colors.primary,
|
color: colors.premium.gold,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
errorBanner: {
|
errorBanner: {
|
||||||
@@ -510,17 +671,31 @@ const styles = StyleSheet.create({
|
|||||||
purchaseButton: {
|
purchaseButton: {
|
||||||
borderRadius: radius.lg,
|
borderRadius: radius.lg,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
shadowColor: colors.premium.gold,
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 8,
|
||||||
},
|
},
|
||||||
purchaseButtonDisabled: {
|
purchaseButtonDisabled: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
|
shadowOpacity: 0,
|
||||||
},
|
},
|
||||||
purchaseButtonGradient: {
|
purchaseButtonLoading: {
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
backgroundColor: colors.premium.gold,
|
||||||
|
},
|
||||||
|
purchaseButtonGradient: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
purchaseButtonText: {
|
purchaseButtonText: {
|
||||||
...typography.headline,
|
...typography.headline,
|
||||||
color: colors.text,
|
color: colors.background,
|
||||||
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
restoreButton: {
|
restoreButton: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -529,7 +704,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
restoreText: {
|
restoreText: {
|
||||||
...typography.subhead,
|
...typography.subhead,
|
||||||
color: colors.primary,
|
color: colors.premium.gold,
|
||||||
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
legal: {
|
legal: {
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
|
|||||||
@@ -0,0 +1,513 @@
|
|||||||
|
/**
|
||||||
|
* SwipeCardPremium - PoC of premium/boutique styling for the swipe experience
|
||||||
|
*
|
||||||
|
* Inspired by the "Coming Soon" Ann Summers aesthetic
|
||||||
|
*/
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { View, Text, StyleSheet, Platform, useWindowDimensions, Pressable } from "react-native";
|
||||||
|
import { Gesture, GestureDetector, TouchableOpacity } from "react-native-gesture-handler";
|
||||||
|
import { BlurView } from "expo-blur";
|
||||||
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
|
import Animated, {
|
||||||
|
useSharedValue,
|
||||||
|
useAnimatedStyle,
|
||||||
|
withSpring,
|
||||||
|
withRepeat,
|
||||||
|
withSequence,
|
||||||
|
runOnJS,
|
||||||
|
interpolate,
|
||||||
|
Extrapolate,
|
||||||
|
withTiming,
|
||||||
|
interpolateColor,
|
||||||
|
Easing,
|
||||||
|
FadeIn,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { colors, gradients, featureColors, radius, shadows, blur, typography, spacing, animations } from "../theme";
|
||||||
|
|
||||||
|
// Use feature colors for consistency
|
||||||
|
const { accent: ACCENT } = featureColors.dares;
|
||||||
|
const { accent: ROSE } = featureColors.quiz;
|
||||||
|
|
||||||
|
// RGBA helpers for custom opacity levels (derived from theme colors)
|
||||||
|
// Gold: #D4AF37 = rgb(212, 175, 55)
|
||||||
|
// Rose: #E8A4AE = rgb(232, 164, 174)
|
||||||
|
const rgba = {
|
||||||
|
gold: (opacity: number) => `rgba(212, 175, 55, ${opacity})`,
|
||||||
|
rose: (opacity: number) => `rgba(232, 164, 174, ${opacity})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Haptics helper
|
||||||
|
const triggerHaptic = async (style: 'light' | 'medium' | 'heavy' = 'medium') => {
|
||||||
|
if (Platform.OS === 'web') return;
|
||||||
|
const Haptics = await import('expo-haptics');
|
||||||
|
const feedbackStyle = style === 'light'
|
||||||
|
? Haptics.ImpactFeedbackStyle.Light
|
||||||
|
: style === 'heavy'
|
||||||
|
? Haptics.ImpactFeedbackStyle.Heavy
|
||||||
|
: Haptics.ImpactFeedbackStyle.Medium;
|
||||||
|
await Haptics.impactAsync(feedbackStyle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_CARD_WIDTH = 400;
|
||||||
|
const SWIPE_THRESHOLD = 100;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
question: { id: string; text: string; intensity: number; partner_text?: string | null; is_two_part?: boolean };
|
||||||
|
onSwipe: (direction: "left" | "right" | "up" | "down") => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SwipeCardPremium({ question, onSwipe }: Props) {
|
||||||
|
const { width: screenWidth } = useWindowDimensions();
|
||||||
|
const cardWidth = Math.min(screenWidth - 48, MAX_CARD_WIDTH);
|
||||||
|
|
||||||
|
const translateX = useSharedValue(0);
|
||||||
|
const translateY = useSharedValue(0);
|
||||||
|
const rotation = useSharedValue(0);
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
const shadowExpand = useSharedValue(0);
|
||||||
|
const idleBreathing = useSharedValue(0);
|
||||||
|
const isGesturing = useSharedValue(false);
|
||||||
|
|
||||||
|
const context = useSharedValue({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// Subtle idle breathing animation
|
||||||
|
useEffect(() => {
|
||||||
|
idleBreathing.value = withRepeat(
|
||||||
|
withSequence(
|
||||||
|
withTiming(1, { duration: 3000, easing: Easing.inOut(Easing.sin) }),
|
||||||
|
withTiming(0, { duration: 3000, easing: Easing.inOut(Easing.sin) })
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const gesture = Gesture.Pan()
|
||||||
|
.activeOffsetX([-15, 15])
|
||||||
|
.activeOffsetY([-15, 15])
|
||||||
|
.onStart(() => {
|
||||||
|
context.value = { x: translateX.value, y: translateY.value };
|
||||||
|
scale.value = withTiming(1.02, { duration: animations.timing.fast });
|
||||||
|
shadowExpand.value = withTiming(1, { duration: animations.timing.fast });
|
||||||
|
isGesturing.value = true;
|
||||||
|
})
|
||||||
|
.onUpdate((event) => {
|
||||||
|
translateX.value = event.translationX + context.value.x;
|
||||||
|
translateY.value = event.translationY + context.value.y;
|
||||||
|
rotation.value = interpolate(
|
||||||
|
translateX.value,
|
||||||
|
[-cardWidth / 2, cardWidth / 2],
|
||||||
|
[-8, 8], // Reduced rotation for elegance
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.onEnd(() => {
|
||||||
|
scale.value = withTiming(1, { duration: animations.timing.fast });
|
||||||
|
shadowExpand.value = withTiming(0, { duration: animations.timing.fast });
|
||||||
|
isGesturing.value = false;
|
||||||
|
|
||||||
|
if (Math.abs(translateX.value) > SWIPE_THRESHOLD) {
|
||||||
|
const direction = translateX.value > 0 ? "right" : "left";
|
||||||
|
runOnJS(triggerHaptic)('medium');
|
||||||
|
translateX.value = withSpring(
|
||||||
|
direction === "right" ? screenWidth * 1.5 : -screenWidth * 1.5,
|
||||||
|
animations.springGentle
|
||||||
|
);
|
||||||
|
runOnJS(onSwipe)(direction);
|
||||||
|
} else if (translateY.value < -SWIPE_THRESHOLD) {
|
||||||
|
runOnJS(triggerHaptic)('medium');
|
||||||
|
translateY.value = withSpring(-screenWidth * 1.5, animations.springGentle);
|
||||||
|
runOnJS(onSwipe)("up");
|
||||||
|
} else if (translateY.value > SWIPE_THRESHOLD) {
|
||||||
|
runOnJS(triggerHaptic)('light');
|
||||||
|
translateY.value = withSpring(screenWidth * 1.5, animations.springGentle);
|
||||||
|
runOnJS(onSwipe)("down");
|
||||||
|
} else {
|
||||||
|
translateX.value = withSpring(0, animations.spring);
|
||||||
|
translateY.value = withSpring(0, animations.spring);
|
||||||
|
rotation.value = withSpring(0, animations.spring);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
|
const idleOffset = isGesturing.value ? 0 : interpolate(idleBreathing.value, [0, 1], [0, -4]);
|
||||||
|
return {
|
||||||
|
transform: [
|
||||||
|
{ translateX: translateX.value },
|
||||||
|
{ translateY: translateY.value + idleOffset },
|
||||||
|
{ rotate: `${rotation.value}deg` },
|
||||||
|
{ scale: scale.value },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardShadowStyle = useAnimatedStyle(() => ({
|
||||||
|
shadowOpacity: interpolate(shadowExpand.value, [0, 1], [0.2, 0.4]),
|
||||||
|
shadowRadius: interpolate(shadowExpand.value, [0, 1], [20, 32]),
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: interpolate(shadowExpand.value, [0, 1], [10, 16]),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const overlayStyle = (direction: "left" | "right" | "up" | "down") =>
|
||||||
|
useAnimatedStyle(() => {
|
||||||
|
let opacity = 0;
|
||||||
|
if (direction === "right") {
|
||||||
|
opacity = interpolate(translateX.value, [0, SWIPE_THRESHOLD], [0, 1]);
|
||||||
|
} else if (direction === "left") {
|
||||||
|
opacity = interpolate(translateX.value, [0, -SWIPE_THRESHOLD], [0, 1]);
|
||||||
|
} else if (direction === "up") {
|
||||||
|
opacity = interpolate(translateY.value, [0, -SWIPE_THRESHOLD], [0, 1]);
|
||||||
|
} else {
|
||||||
|
opacity = interpolate(translateY.value, [0, SWIPE_THRESHOLD], [0, 1]);
|
||||||
|
}
|
||||||
|
return { opacity };
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleButtonPress = async (direction: "left" | "right" | "up") => {
|
||||||
|
await triggerHaptic('light');
|
||||||
|
onSwipe(direction);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.cardWrapper, { width: cardWidth }]}>
|
||||||
|
<GestureDetector gesture={gesture}>
|
||||||
|
<Animated.View style={[styles.cardOuter, animatedStyle, cardShadowStyle, { shadowColor: ACCENT }]}>
|
||||||
|
{/* Premium dark background */}
|
||||||
|
<View style={styles.cardBackground}>
|
||||||
|
{/* Subtle gradient overlay */}
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(22, 33, 62, 0.95)', 'rgba(13, 13, 26, 0.98)']}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
start={{ x: 0.5, y: 0 }}
|
||||||
|
end={{ x: 0.5, y: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Top silk highlight */}
|
||||||
|
<LinearGradient
|
||||||
|
colors={[rgba.gold(0.08), 'transparent']}
|
||||||
|
style={styles.silkHighlight}
|
||||||
|
start={{ x: 0.5, y: 0 }}
|
||||||
|
end={{ x: 0.5, y: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Swipe Overlays - Premium style */}
|
||||||
|
<Animated.View style={[styles.overlay, styles.overlayYes, overlayStyle("right")]} pointerEvents="none">
|
||||||
|
<View style={styles.overlayBadgePremium}>
|
||||||
|
<Text style={styles.overlayTextPremium}>YES</Text>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
<Animated.View style={[styles.overlay, styles.overlayNo, overlayStyle("left")]} pointerEvents="none">
|
||||||
|
<View style={[styles.overlayBadgePremium, styles.overlayBadgeNo]}>
|
||||||
|
<Text style={[styles.overlayTextPremium, styles.overlayTextNo]}>NO</Text>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
<Animated.View style={[styles.overlay, styles.overlayMaybe, overlayStyle("up")]} pointerEvents="none">
|
||||||
|
<View style={[styles.overlayBadgePremium, styles.overlayBadgeMaybe]}>
|
||||||
|
<Text style={[styles.overlayTextPremium, styles.overlayTextMaybe]}>MAYBE</Text>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
<Animated.View style={[styles.overlay, styles.overlaySkip, overlayStyle("down")]} pointerEvents="none">
|
||||||
|
<View style={[styles.overlayBadgePremium, styles.overlayBadgeSkip]}>
|
||||||
|
<Text style={[styles.overlayTextPremium, styles.overlayTextSkip]}>SKIP</Text>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<View style={styles.content}>
|
||||||
|
{/* Intensity badge - more prominent */}
|
||||||
|
<View style={styles.intensityBadge}>
|
||||||
|
<Text style={styles.intensityLabel}>INTENSITY</Text>
|
||||||
|
<View style={styles.intensityRow}>
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Ionicons
|
||||||
|
key={i}
|
||||||
|
name={i < question.intensity ? "flame" : "flame-outline"}
|
||||||
|
size={18}
|
||||||
|
color={i < question.intensity ? ACCENT : 'rgba(255, 255, 255, 0.2)'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Question text */}
|
||||||
|
<Text style={styles.questionText}>
|
||||||
|
{question.text}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Bottom separator */}
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Premium Action Buttons */}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<PremiumButton
|
||||||
|
onPress={() => handleButtonPress("left")}
|
||||||
|
icon="close"
|
||||||
|
variant="no"
|
||||||
|
/>
|
||||||
|
<PremiumButton
|
||||||
|
onPress={() => handleButtonPress("up")}
|
||||||
|
icon="help"
|
||||||
|
variant="maybe"
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
<PremiumButton
|
||||||
|
onPress={() => handleButtonPress("right")}
|
||||||
|
icon="heart"
|
||||||
|
variant="yes"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Premium border */}
|
||||||
|
<View style={styles.premiumBorder} pointerEvents="none" />
|
||||||
|
</Animated.View>
|
||||||
|
</GestureDetector>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PremiumButton({
|
||||||
|
onPress,
|
||||||
|
icon,
|
||||||
|
variant,
|
||||||
|
small = false,
|
||||||
|
}: {
|
||||||
|
onPress: () => void;
|
||||||
|
icon: string;
|
||||||
|
variant: 'yes' | 'no' | 'maybe';
|
||||||
|
small?: boolean;
|
||||||
|
}) {
|
||||||
|
const buttonScale = useSharedValue(1);
|
||||||
|
|
||||||
|
const handlePressIn = () => {
|
||||||
|
buttonScale.value = withSpring(0.92, animations.spring);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePressOut = () => {
|
||||||
|
buttonScale.value = withSpring(1, animations.spring);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ scale: buttonScale.value }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getColors = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'yes': return { bg: rgba.gold(0.15), border: rgba.gold(0.3), icon: ACCENT };
|
||||||
|
case 'no': return { bg: rgba.rose(0.15), border: rgba.rose(0.3), icon: ROSE };
|
||||||
|
case 'maybe': return { bg: 'rgba(255, 255, 255, 0.08)', border: 'rgba(255, 255, 255, 0.15)', icon: colors.textSecondary };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const c = getColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
onPressIn={handlePressIn}
|
||||||
|
onPressOut={handlePressOut}
|
||||||
|
activeOpacity={1}
|
||||||
|
>
|
||||||
|
<Animated.View style={[
|
||||||
|
styles.premiumButton,
|
||||||
|
small && styles.premiumButtonSmall,
|
||||||
|
{ backgroundColor: c.bg, borderColor: c.border },
|
||||||
|
buttonAnimatedStyle,
|
||||||
|
]}>
|
||||||
|
<Ionicons
|
||||||
|
name={icon as any}
|
||||||
|
size={small ? 18 : 24}
|
||||||
|
color={c.icon}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
cardWrapper: {
|
||||||
|
position: "absolute",
|
||||||
|
maxWidth: MAX_CARD_WIDTH,
|
||||||
|
height: 500,
|
||||||
|
},
|
||||||
|
cardOuter: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: radius.xl,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
cardBackground: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#0d0d1a',
|
||||||
|
borderRadius: radius.xl,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
silkHighlight: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 120,
|
||||||
|
},
|
||||||
|
premiumBorder: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
borderRadius: radius.xl,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.2)', // gold @ 0.2
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: spacing.xl,
|
||||||
|
},
|
||||||
|
intensityBadge: {
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.08)', // gold @ 0.08
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: radius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.15)', // gold @ 0.15
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
intensityLabel: {
|
||||||
|
...typography.caption2,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: 'rgba(212, 175, 55, 0.7)', // gold @ 0.7
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
intensityRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: spacing.lg,
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
separatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.2)', // gold @ 0.2
|
||||||
|
},
|
||||||
|
separatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: ACCENT,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
questionText: {
|
||||||
|
...typography.title2,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: "center",
|
||||||
|
lineHeight: 32,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
paddingBottom: spacing.xl,
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: spacing.lg,
|
||||||
|
},
|
||||||
|
premiumButton: {
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
borderRadius: 30,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
premiumButtonSmall: {
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 24,
|
||||||
|
},
|
||||||
|
// Overlays
|
||||||
|
overlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
zIndex: 10,
|
||||||
|
borderRadius: radius.xl,
|
||||||
|
},
|
||||||
|
overlayYes: {
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.15)', // gold @ 0.15
|
||||||
|
},
|
||||||
|
overlayNo: {
|
||||||
|
backgroundColor: 'rgba(232, 164, 174, 0.15)', // rose @ 0.15
|
||||||
|
},
|
||||||
|
overlayMaybe: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
},
|
||||||
|
overlaySkip: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
},
|
||||||
|
overlayBadgePremium: {
|
||||||
|
paddingHorizontal: spacing.xl,
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.2)', // gold @ 0.2
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.4)', // gold @ 0.4
|
||||||
|
transform: [{ rotate: "-12deg" }],
|
||||||
|
},
|
||||||
|
overlayBadgeNo: {
|
||||||
|
backgroundColor: 'rgba(232, 164, 174, 0.2)', // rose @ 0.2
|
||||||
|
borderColor: 'rgba(232, 164, 174, 0.4)', // rose @ 0.4
|
||||||
|
},
|
||||||
|
overlayBadgeMaybe: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
},
|
||||||
|
overlayBadgeSkip: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
},
|
||||||
|
overlayTextPremium: {
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: "600",
|
||||||
|
letterSpacing: 4,
|
||||||
|
color: ACCENT,
|
||||||
|
},
|
||||||
|
overlayTextNo: {
|
||||||
|
color: ROSE,
|
||||||
|
},
|
||||||
|
overlayTextMaybe: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
overlayTextSkip: {
|
||||||
|
color: colors.textTertiary,
|
||||||
|
fontSize: 24,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { supabase } from './supabase';
|
import { supabase } from './supabase';
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
// Cache for signed URLs with expiration tracking
|
// Cache for signed URLs with expiration tracking
|
||||||
interface CachedUrl {
|
interface CachedUrl {
|
||||||
@@ -8,6 +10,12 @@ interface CachedUrl {
|
|||||||
|
|
||||||
const signedUrlCache = new Map<string, CachedUrl>();
|
const signedUrlCache = new Map<string, CachedUrl>();
|
||||||
|
|
||||||
|
// In-memory tracking of videos currently being cached to prevent duplicate downloads
|
||||||
|
const videoCacheInProgress = new Set<string>();
|
||||||
|
|
||||||
|
// Video cache directory
|
||||||
|
const VIDEO_CACHE_DIR = `${FileSystem.cacheDirectory}video-cache/`;
|
||||||
|
|
||||||
// Buffer time before expiration to refresh URL (5 minutes)
|
// Buffer time before expiration to refresh URL (5 minutes)
|
||||||
const EXPIRATION_BUFFER_MS = 5 * 60 * 1000;
|
const EXPIRATION_BUFFER_MS = 5 * 60 * 1000;
|
||||||
// Default signed URL expiry (1 hour)
|
// Default signed URL expiry (1 hour)
|
||||||
@@ -93,3 +101,134 @@ export async function prefetchSignedUrls(
|
|||||||
mediaPaths.map(path => getCachedSignedUrl(getStoragePath(path), bucket))
|
mediaPaths.map(path => getCachedSignedUrl(getStoragePath(path), bucket))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the video cache directory exists.
|
||||||
|
*/
|
||||||
|
async function ensureVideoCacheDir(): Promise<void> {
|
||||||
|
if (Platform.OS === 'web') return;
|
||||||
|
|
||||||
|
const dirInfo = await FileSystem.getInfoAsync(VIDEO_CACHE_DIR);
|
||||||
|
if (!dirInfo.exists) {
|
||||||
|
await FileSystem.makeDirectoryAsync(VIDEO_CACHE_DIR, { intermediates: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a safe filename from a storage path.
|
||||||
|
*/
|
||||||
|
function getVideoCacheFilename(storagePath: string): string {
|
||||||
|
// Replace slashes and other unsafe chars with underscores
|
||||||
|
return storagePath.replace(/[\/\\:*?"<>|]/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the local cache path for a video.
|
||||||
|
*/
|
||||||
|
export function getVideoCachePath(storagePath: string): string {
|
||||||
|
return `${VIDEO_CACHE_DIR}${getVideoCacheFilename(storagePath)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a video is cached locally.
|
||||||
|
* Returns the local file URI if cached, null otherwise.
|
||||||
|
*/
|
||||||
|
export async function getVideoCachedUri(storagePath: string): Promise<string | null> {
|
||||||
|
if (Platform.OS === 'web') return null;
|
||||||
|
|
||||||
|
const cachePath = getVideoCachePath(storagePath);
|
||||||
|
const fileInfo = await FileSystem.getInfoAsync(cachePath);
|
||||||
|
|
||||||
|
if (fileInfo.exists) {
|
||||||
|
return cachePath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache a video to local storage from a signed URL.
|
||||||
|
* Returns the local file URI on success, null on failure.
|
||||||
|
*/
|
||||||
|
export async function cacheVideo(
|
||||||
|
storagePath: string,
|
||||||
|
signedUrl: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (Platform.OS === 'web') return null;
|
||||||
|
|
||||||
|
// Check if already cached
|
||||||
|
const existingCache = await getVideoCachedUri(storagePath);
|
||||||
|
if (existingCache) {
|
||||||
|
return existingCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if caching is already in progress for this video
|
||||||
|
if (videoCacheInProgress.has(storagePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
videoCacheInProgress.add(storagePath);
|
||||||
|
await ensureVideoCacheDir();
|
||||||
|
|
||||||
|
const cachePath = getVideoCachePath(storagePath);
|
||||||
|
|
||||||
|
// Download the video file
|
||||||
|
const downloadResult = await FileSystem.downloadAsync(signedUrl, cachePath);
|
||||||
|
|
||||||
|
if (downloadResult.status === 200) {
|
||||||
|
console.log('Video cached successfully:', storagePath);
|
||||||
|
return cachePath;
|
||||||
|
} else {
|
||||||
|
console.warn('Video cache download failed with status:', downloadResult.status);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to cache video:', error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
videoCacheInProgress.delete(storagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cached videos.
|
||||||
|
*/
|
||||||
|
export async function clearVideoCache(): Promise<void> {
|
||||||
|
if (Platform.OS === 'web') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dirInfo = await FileSystem.getInfoAsync(VIDEO_CACHE_DIR);
|
||||||
|
if (dirInfo.exists) {
|
||||||
|
await FileSystem.deleteAsync(VIDEO_CACHE_DIR, { idempotent: true });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear video cache:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of the video cache in bytes.
|
||||||
|
*/
|
||||||
|
export async function getVideoCacheSize(): Promise<number> {
|
||||||
|
if (Platform.OS === 'web') return 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dirInfo = await FileSystem.getInfoAsync(VIDEO_CACHE_DIR);
|
||||||
|
if (!dirInfo.exists) return 0;
|
||||||
|
|
||||||
|
const files = await FileSystem.readDirectoryAsync(VIDEO_CACHE_DIR);
|
||||||
|
let totalSize = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fileInfo = await FileSystem.getInfoAsync(`${VIDEO_CACHE_DIR}${file}`);
|
||||||
|
if (fileInfo.exists && 'size' in fileInfo) {
|
||||||
|
totalSize += fileInfo.size || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalSize;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get video cache size:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -211,8 +211,18 @@ export const useMatchStore = create<MatchState>((set, get) => ({
|
|||||||
unreadCount: unreadCounts[match.id] || 0
|
unreadCount: unreadCounts[match.id] || 0
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const newCount = data.filter((m) => m.is_new).length;
|
// Sort: unread messages first, then by most recent
|
||||||
set({ matches: data, newMatchesCount: newCount });
|
const sortedData = data.sort((a, b) => {
|
||||||
|
// First priority: matches with unread messages
|
||||||
|
if (a.unreadCount > 0 && b.unreadCount === 0) return -1;
|
||||||
|
if (a.unreadCount === 0 && b.unreadCount > 0) return 1;
|
||||||
|
|
||||||
|
// Second priority: most recent first
|
||||||
|
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
const newCount = sortedData.filter((m) => m.is_new).length;
|
||||||
|
set({ matches: sortedData, newMatchesCount: newCount });
|
||||||
},
|
},
|
||||||
|
|
||||||
markAsSeen: async (matchId) => {
|
markAsSeen: async (matchId) => {
|
||||||
|
|||||||
@@ -42,13 +42,28 @@ export const colors = {
|
|||||||
// Premium/Luxury
|
// Premium/Luxury
|
||||||
premium: {
|
premium: {
|
||||||
gold: '#D4AF37',
|
gold: '#D4AF37',
|
||||||
|
goldDark: '#B8860B',
|
||||||
goldLight: 'rgba(212, 175, 55, 0.15)',
|
goldLight: 'rgba(212, 175, 55, 0.15)',
|
||||||
goldGlow: 'rgba(212, 175, 55, 0.3)',
|
goldGlow: 'rgba(212, 175, 55, 0.3)',
|
||||||
champagne: '#F7E7CE',
|
champagne: '#F7E7CE',
|
||||||
champagneLight: 'rgba(247, 231, 206, 0.1)',
|
champagneLight: 'rgba(247, 231, 206, 0.1)',
|
||||||
rose: '#E8A4AE', // Lightened for accessibility (was #B76E79, now ~5.5:1 contrast)
|
rose: '#E8A4AE',
|
||||||
|
roseDark: '#D4919B',
|
||||||
roseLight: 'rgba(232, 164, 174, 0.15)',
|
roseLight: 'rgba(232, 164, 174, 0.15)',
|
||||||
|
roseGlow: 'rgba(232, 164, 174, 0.3)',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Swipe response overlays
|
||||||
|
overlay: {
|
||||||
|
yes: 'rgba(46, 204, 113, 0.4)',
|
||||||
|
no: 'rgba(231, 76, 60, 0.4)',
|
||||||
|
maybe: 'rgba(243, 156, 18, 0.4)',
|
||||||
|
skip: 'rgba(108, 117, 125, 0.4)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Muted/disabled
|
||||||
|
muted: '#6c757d',
|
||||||
|
mutedLight: 'rgba(108, 117, 125, 0.15)',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gradients = {
|
export const gradients = {
|
||||||
@@ -64,7 +79,7 @@ export const gradients = {
|
|||||||
error: ['#E74C3C', '#c0392b'],
|
error: ['#E74C3C', '#c0392b'],
|
||||||
// Premium/Boutique gradients
|
// Premium/Boutique gradients
|
||||||
premiumGold: ['#D4AF37', '#B8860B'],
|
premiumGold: ['#D4AF37', '#B8860B'],
|
||||||
premiumRose: ['#B76E79', '#8B4557'],
|
premiumRose: ['#E8A4AE', '#D4919B'],
|
||||||
silkLight: ['rgba(255, 255, 255, 0.08)', 'rgba(255, 255, 255, 0.02)'],
|
silkLight: ['rgba(255, 255, 255, 0.08)', 'rgba(255, 255, 255, 0.02)'],
|
||||||
laceTint: ['rgba(233, 69, 96, 0.15)', 'rgba(155, 89, 182, 0.15)'],
|
laceTint: ['rgba(233, 69, 96, 0.15)', 'rgba(155, 89, 182, 0.15)'],
|
||||||
// Boutique pack card backgrounds
|
// Boutique pack card backgrounds
|
||||||
@@ -76,6 +91,55 @@ export const gradients = {
|
|||||||
boutiqueAmethyst: ['rgba(142, 68, 173, 0.4)', 'rgba(44, 62, 80, 0.4)'],
|
boutiqueAmethyst: ['rgba(142, 68, 173, 0.4)', 'rgba(44, 62, 80, 0.4)'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Feature-specific color mapping
|
||||||
|
// Each feature has a consistent accent color for icons, buttons, and highlights
|
||||||
|
export const featureColors = {
|
||||||
|
swipe: {
|
||||||
|
accent: colors.primary,
|
||||||
|
accentDark: colors.primaryDark,
|
||||||
|
accentLight: colors.primaryLight,
|
||||||
|
accentGlow: colors.primaryGlow,
|
||||||
|
gradient: gradients.primary,
|
||||||
|
},
|
||||||
|
match: {
|
||||||
|
accent: colors.primary,
|
||||||
|
accentDark: colors.primaryDark,
|
||||||
|
accentLight: colors.primaryLight,
|
||||||
|
accentGlow: colors.primaryGlow,
|
||||||
|
gradient: gradients.primary,
|
||||||
|
},
|
||||||
|
quiz: {
|
||||||
|
accent: colors.premium.rose,
|
||||||
|
accentDark: colors.premium.roseDark,
|
||||||
|
accentLight: colors.premium.roseLight,
|
||||||
|
accentGlow: colors.premium.roseGlow,
|
||||||
|
gradient: gradients.premiumRose,
|
||||||
|
},
|
||||||
|
dares: {
|
||||||
|
accent: colors.premium.gold,
|
||||||
|
accentDark: colors.premium.goldDark,
|
||||||
|
accentLight: colors.premium.goldLight,
|
||||||
|
accentGlow: colors.premium.goldGlow,
|
||||||
|
gradient: gradients.premiumGold,
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
accent: colors.primary,
|
||||||
|
accentDark: colors.primaryDark,
|
||||||
|
accentLight: colors.primaryLight,
|
||||||
|
accentGlow: colors.primaryGlow,
|
||||||
|
gradient: gradients.primary,
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
accent: colors.secondary,
|
||||||
|
accentDark: colors.secondaryDark,
|
||||||
|
accentLight: colors.secondaryLight,
|
||||||
|
accentGlow: colors.secondaryGlow,
|
||||||
|
gradient: gradients.primaryReverse,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type FeatureName = keyof typeof featureColors;
|
||||||
|
|
||||||
export const spacing = {
|
export const spacing = {
|
||||||
xs: 4,
|
xs: 4,
|
||||||
sm: 8,
|
sm: 8,
|
||||||
@@ -222,6 +286,7 @@ export const animations = {
|
|||||||
export const theme = {
|
export const theme = {
|
||||||
colors,
|
colors,
|
||||||
gradients,
|
gradients,
|
||||||
|
featureColors,
|
||||||
spacing,
|
spacing,
|
||||||
radius,
|
radius,
|
||||||
typography,
|
typography,
|
||||||
|
|||||||
+638
-100
@@ -14,33 +14,152 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
public: {
|
public: {
|
||||||
Tables: {
|
Tables: {
|
||||||
categories: {
|
admin_users: {
|
||||||
Row: {
|
Row: {
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string | null
|
|
||||||
icon: string | null
|
|
||||||
sort_order: number | null
|
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
|
created_by: string | null
|
||||||
|
id: string
|
||||||
|
permissions: Json | null
|
||||||
|
role: Database["public"]["Enums"]["admin_role"]
|
||||||
|
updated_at: string | null
|
||||||
|
user_id: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
id?: string
|
|
||||||
name: string
|
|
||||||
description?: string | null
|
|
||||||
icon?: string | null
|
|
||||||
sort_order?: number | null
|
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
|
created_by?: string | null
|
||||||
|
id?: string
|
||||||
|
permissions?: Json | null
|
||||||
|
role: Database["public"]["Enums"]["admin_role"]
|
||||||
|
updated_at?: string | null
|
||||||
|
user_id: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
id?: string
|
|
||||||
name?: string
|
|
||||||
description?: string | null
|
|
||||||
icon?: string | null
|
|
||||||
sort_order?: number | null
|
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
|
created_by?: string | null
|
||||||
|
id?: string
|
||||||
|
permissions?: Json | null
|
||||||
|
role?: Database["public"]["Enums"]["admin_role"]
|
||||||
|
updated_at?: string | null
|
||||||
|
user_id?: string
|
||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
}
|
}
|
||||||
|
app_config: {
|
||||||
|
Row: {
|
||||||
|
answer_gap_threshold: number | null
|
||||||
|
id: string
|
||||||
|
updated_at: string | null
|
||||||
|
updated_by: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
answer_gap_threshold?: number | null
|
||||||
|
id?: string
|
||||||
|
updated_at?: string | null
|
||||||
|
updated_by?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
answer_gap_threshold?: number | null
|
||||||
|
id?: string
|
||||||
|
updated_at?: string | null
|
||||||
|
updated_by?: string | null
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
audit_logs: {
|
||||||
|
Row: {
|
||||||
|
action: Database["public"]["Enums"]["audit_action"]
|
||||||
|
admin_role: Database["public"]["Enums"]["admin_role"]
|
||||||
|
admin_user_id: string
|
||||||
|
changed_fields: string[] | null
|
||||||
|
created_at: string | null
|
||||||
|
id: string
|
||||||
|
new_values: Json | null
|
||||||
|
old_values: Json | null
|
||||||
|
record_id: string
|
||||||
|
table_name: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
action: Database["public"]["Enums"]["audit_action"]
|
||||||
|
admin_role: Database["public"]["Enums"]["admin_role"]
|
||||||
|
admin_user_id: string
|
||||||
|
changed_fields?: string[] | null
|
||||||
|
created_at?: string | null
|
||||||
|
id?: string
|
||||||
|
new_values?: Json | null
|
||||||
|
old_values?: Json | null
|
||||||
|
record_id: string
|
||||||
|
table_name: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
action?: Database["public"]["Enums"]["audit_action"]
|
||||||
|
admin_role?: Database["public"]["Enums"]["admin_role"]
|
||||||
|
admin_user_id?: string
|
||||||
|
changed_fields?: string[] | null
|
||||||
|
created_at?: string | null
|
||||||
|
id?: string
|
||||||
|
new_values?: Json | null
|
||||||
|
old_values?: Json | null
|
||||||
|
record_id?: string
|
||||||
|
table_name?: string
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
categories: {
|
||||||
|
Row: {
|
||||||
|
created_at: string | null
|
||||||
|
description: string | null
|
||||||
|
icon: string | null
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
sort_order: number | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
created_at?: string | null
|
||||||
|
description?: string | null
|
||||||
|
icon?: string | null
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
sort_order?: number | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
created_at?: string | null
|
||||||
|
description?: string | null
|
||||||
|
icon?: string | null
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
sort_order?: number | null
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
code_redemptions: {
|
||||||
|
Row: {
|
||||||
|
code_id: string
|
||||||
|
id: string
|
||||||
|
redeemed_at: string | null
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
code_id: string
|
||||||
|
id?: string
|
||||||
|
redeemed_at?: string | null
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
code_id?: string
|
||||||
|
id?: string
|
||||||
|
redeemed_at?: string | null
|
||||||
|
user_id?: string
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "code_redemptions_code_id_fkey"
|
||||||
|
columns: ["code_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "redemption_codes"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
couple_packs: {
|
couple_packs: {
|
||||||
Row: {
|
Row: {
|
||||||
couple_id: string
|
couple_id: string
|
||||||
@@ -95,6 +214,87 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
}
|
}
|
||||||
|
feature_interests: {
|
||||||
|
Row: {
|
||||||
|
created_at: string
|
||||||
|
feature_name: string
|
||||||
|
id: string
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
created_at?: string
|
||||||
|
feature_name: string
|
||||||
|
id?: string
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
created_at?: string
|
||||||
|
feature_name?: string
|
||||||
|
id?: string
|
||||||
|
user_id?: string
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
feedback: {
|
||||||
|
Row: {
|
||||||
|
admin_notes: string | null
|
||||||
|
created_at: string | null
|
||||||
|
description: string
|
||||||
|
device_info: Json
|
||||||
|
id: string
|
||||||
|
question_id: string | null
|
||||||
|
screenshot_url: string | null
|
||||||
|
status: Database["public"]["Enums"]["feedback_status"]
|
||||||
|
title: string
|
||||||
|
type: Database["public"]["Enums"]["feedback_type"]
|
||||||
|
updated_at: string | null
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
admin_notes?: string | null
|
||||||
|
created_at?: string | null
|
||||||
|
description: string
|
||||||
|
device_info?: Json
|
||||||
|
id?: string
|
||||||
|
question_id?: string | null
|
||||||
|
screenshot_url?: string | null
|
||||||
|
status?: Database["public"]["Enums"]["feedback_status"]
|
||||||
|
title: string
|
||||||
|
type: Database["public"]["Enums"]["feedback_type"]
|
||||||
|
updated_at?: string | null
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
admin_notes?: string | null
|
||||||
|
created_at?: string | null
|
||||||
|
description?: string
|
||||||
|
device_info?: Json
|
||||||
|
id?: string
|
||||||
|
question_id?: string | null
|
||||||
|
screenshot_url?: string | null
|
||||||
|
status?: Database["public"]["Enums"]["feedback_status"]
|
||||||
|
title?: string
|
||||||
|
type?: Database["public"]["Enums"]["feedback_type"]
|
||||||
|
updated_at?: string | null
|
||||||
|
user_id?: string
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "feedback_question_id_fkey"
|
||||||
|
columns: ["question_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "questions"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "feedback_user_id_fkey"
|
||||||
|
columns: ["user_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "profiles"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
matches: {
|
matches: {
|
||||||
Row: {
|
Row: {
|
||||||
couple_id: string
|
couple_id: string
|
||||||
@@ -144,7 +344,10 @@ export type Database = {
|
|||||||
delivered_at: string | null
|
delivered_at: string | null
|
||||||
id: string
|
id: string
|
||||||
match_id: string
|
match_id: string
|
||||||
|
media_expired: boolean | null
|
||||||
|
media_expires_at: string | null
|
||||||
media_path: string | null
|
media_path: string | null
|
||||||
|
media_type: string | null
|
||||||
media_viewed_at: string | null
|
media_viewed_at: string | null
|
||||||
read_at: string | null
|
read_at: string | null
|
||||||
user_id: string
|
user_id: string
|
||||||
@@ -155,7 +358,10 @@ export type Database = {
|
|||||||
delivered_at?: string | null
|
delivered_at?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
match_id: string
|
match_id: string
|
||||||
|
media_expired?: boolean | null
|
||||||
|
media_expires_at?: string | null
|
||||||
media_path?: string | null
|
media_path?: string | null
|
||||||
|
media_type?: string | null
|
||||||
media_viewed_at?: string | null
|
media_viewed_at?: string | null
|
||||||
read_at?: string | null
|
read_at?: string | null
|
||||||
user_id: string
|
user_id: string
|
||||||
@@ -166,7 +372,10 @@ export type Database = {
|
|||||||
delivered_at?: string | null
|
delivered_at?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
match_id?: string
|
match_id?: string
|
||||||
|
media_expired?: boolean | null
|
||||||
|
media_expires_at?: string | null
|
||||||
media_path?: string | null
|
media_path?: string | null
|
||||||
|
media_type?: string | null
|
||||||
media_viewed_at?: string | null
|
media_viewed_at?: string | null
|
||||||
read_at?: string | null
|
read_at?: string | null
|
||||||
user_id?: string
|
user_id?: string
|
||||||
@@ -181,39 +390,81 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
pack_topics: {
|
||||||
|
Row: {
|
||||||
|
pack_id: string
|
||||||
|
topic_id: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
pack_id: string
|
||||||
|
topic_id: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
pack_id?: string
|
||||||
|
topic_id?: string
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "pack_topics_pack_id_fkey"
|
||||||
|
columns: ["pack_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "question_packs"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "pack_topics_topic_id_fkey"
|
||||||
|
columns: ["topic_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "topics"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
profiles: {
|
profiles: {
|
||||||
Row: {
|
Row: {
|
||||||
avatar_url: string | null
|
avatar_url: string | null
|
||||||
couple_id: string | null
|
couple_id: string | null
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
email: string | null
|
email: string | null
|
||||||
|
gender: string | null
|
||||||
id: string
|
id: string
|
||||||
is_premium: boolean | null
|
is_premium: boolean | null
|
||||||
name: string | null
|
name: string | null
|
||||||
|
onboarding_completed: boolean | null
|
||||||
push_token: string | null
|
push_token: string | null
|
||||||
|
show_explicit_content: boolean | null
|
||||||
updated_at: string | null
|
updated_at: string | null
|
||||||
|
usage_reason: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
avatar_url?: string | null
|
avatar_url?: string | null
|
||||||
couple_id?: string | null
|
couple_id?: string | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
email?: string | null
|
email?: string | null
|
||||||
|
gender?: string | null
|
||||||
id: string
|
id: string
|
||||||
is_premium?: boolean | null
|
is_premium?: boolean | null
|
||||||
name?: string | null
|
name?: string | null
|
||||||
|
onboarding_completed?: boolean | null
|
||||||
push_token?: string | null
|
push_token?: string | null
|
||||||
|
show_explicit_content?: boolean | null
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
|
usage_reason?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
avatar_url?: string | null
|
avatar_url?: string | null
|
||||||
couple_id?: string | null
|
couple_id?: string | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
email?: string | null
|
email?: string | null
|
||||||
|
gender?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
is_premium?: boolean | null
|
is_premium?: boolean | null
|
||||||
name?: string | null
|
name?: string | null
|
||||||
|
onboarding_completed?: boolean | null
|
||||||
push_token?: string | null
|
push_token?: string | null
|
||||||
|
show_explicit_content?: boolean | null
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
|
usage_reason?: string | null
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
{
|
{
|
||||||
@@ -227,37 +478,40 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
question_packs: {
|
question_packs: {
|
||||||
Row: {
|
Row: {
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string | null
|
|
||||||
icon: string | null
|
|
||||||
is_premium: boolean | null
|
|
||||||
is_public: boolean | null
|
|
||||||
sort_order: number | null
|
|
||||||
category_id: string | null
|
category_id: string | null
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
|
description: string | null
|
||||||
|
icon: string | null
|
||||||
|
id: string
|
||||||
|
is_explicit: boolean
|
||||||
|
is_premium: boolean | null
|
||||||
|
is_public: boolean | null
|
||||||
|
name: string
|
||||||
|
sort_order: number | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
id?: string
|
|
||||||
name: string
|
|
||||||
description?: string | null
|
|
||||||
icon?: string | null
|
|
||||||
is_premium?: boolean | null
|
|
||||||
is_public?: boolean | null
|
|
||||||
sort_order?: number | null
|
|
||||||
category_id?: string | null
|
category_id?: string | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
|
description?: string | null
|
||||||
|
icon?: string | null
|
||||||
|
id?: string
|
||||||
|
is_explicit?: boolean
|
||||||
|
is_premium?: boolean | null
|
||||||
|
is_public?: boolean | null
|
||||||
|
name: string
|
||||||
|
sort_order?: number | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
id?: string
|
|
||||||
name?: string
|
|
||||||
description?: string | null
|
|
||||||
icon?: string | null
|
|
||||||
is_premium?: boolean | null
|
|
||||||
is_public?: boolean | null
|
|
||||||
sort_order?: number | null
|
|
||||||
category_id?: string | null
|
category_id?: string | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
|
description?: string | null
|
||||||
|
icon?: string | null
|
||||||
|
id?: string
|
||||||
|
is_explicit?: boolean
|
||||||
|
is_premium?: boolean | null
|
||||||
|
is_public?: boolean | null
|
||||||
|
name?: string
|
||||||
|
sort_order?: number | null
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
{
|
{
|
||||||
@@ -266,33 +520,39 @@ export type Database = {
|
|||||||
isOneToOne: false
|
isOneToOne: false
|
||||||
referencedRelation: "categories"
|
referencedRelation: "categories"
|
||||||
referencedColumns: ["id"]
|
referencedColumns: ["id"]
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
questions: {
|
questions: {
|
||||||
Row: {
|
Row: {
|
||||||
|
allowed_couple_genders: string[] | null
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
id: string
|
id: string
|
||||||
intensity: number | null
|
intensity: number | null
|
||||||
pack_id: string
|
pack_id: string
|
||||||
text: string
|
|
||||||
partner_text: string | null
|
partner_text: string | null
|
||||||
|
target_user_genders: string[] | null
|
||||||
|
text: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
|
allowed_couple_genders?: string[] | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
intensity?: number | null
|
intensity?: number | null
|
||||||
pack_id: string
|
pack_id: string
|
||||||
text: string
|
|
||||||
partner_text?: string | null
|
partner_text?: string | null
|
||||||
|
target_user_genders?: string[] | null
|
||||||
|
text: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
|
allowed_couple_genders?: string[] | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
intensity?: number | null
|
intensity?: number | null
|
||||||
pack_id?: string
|
pack_id?: string
|
||||||
text?: string
|
|
||||||
partner_text?: string | null
|
partner_text?: string | null
|
||||||
|
target_user_genders?: string[] | null
|
||||||
|
text?: string
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
{
|
{
|
||||||
@@ -304,6 +564,45 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
redemption_codes: {
|
||||||
|
Row: {
|
||||||
|
code: string
|
||||||
|
created_at: string | null
|
||||||
|
created_by: string | null
|
||||||
|
current_uses: number | null
|
||||||
|
description: string | null
|
||||||
|
expires_at: string | null
|
||||||
|
id: string
|
||||||
|
is_active: boolean | null
|
||||||
|
max_uses: number | null
|
||||||
|
updated_at: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
code: string
|
||||||
|
created_at?: string | null
|
||||||
|
created_by?: string | null
|
||||||
|
current_uses?: number | null
|
||||||
|
description?: string | null
|
||||||
|
expires_at?: string | null
|
||||||
|
id?: string
|
||||||
|
is_active?: boolean | null
|
||||||
|
max_uses?: number | null
|
||||||
|
updated_at?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
code?: string
|
||||||
|
created_at?: string | null
|
||||||
|
created_by?: string | null
|
||||||
|
current_uses?: number | null
|
||||||
|
description?: string | null
|
||||||
|
expires_at?: string | null
|
||||||
|
id?: string
|
||||||
|
is_active?: boolean | null
|
||||||
|
max_uses?: number | null
|
||||||
|
updated_at?: string | null
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
responses: {
|
responses: {
|
||||||
Row: {
|
Row: {
|
||||||
answer: Database["public"]["Enums"]["answer_type"]
|
answer: Database["public"]["Enums"]["answer_type"]
|
||||||
@@ -344,31 +643,245 @@ export type Database = {
|
|||||||
referencedRelation: "questions"
|
referencedRelation: "questions"
|
||||||
referencedColumns: ["id"]
|
referencedColumns: ["id"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "responses_user_id_fkey"
|
||||||
|
columns: ["user_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "profiles"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
revenuecat_webhook_events: {
|
||||||
|
Row: {
|
||||||
|
app_user_id: string
|
||||||
|
event_id: string
|
||||||
|
event_type: string
|
||||||
|
id: string
|
||||||
|
payload: Json | null
|
||||||
|
processed_at: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
app_user_id: string
|
||||||
|
event_id: string
|
||||||
|
event_type: string
|
||||||
|
id?: string
|
||||||
|
payload?: Json | null
|
||||||
|
processed_at?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
app_user_id?: string
|
||||||
|
event_id?: string
|
||||||
|
event_type?: string
|
||||||
|
id?: string
|
||||||
|
payload?: Json | null
|
||||||
|
processed_at?: string | null
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
subscriptions: {
|
||||||
|
Row: {
|
||||||
|
cancel_reason: string | null
|
||||||
|
created_at: string | null
|
||||||
|
entitlement_ids: string[] | null
|
||||||
|
expires_at: string | null
|
||||||
|
grace_period_expires_at: string | null
|
||||||
|
id: string
|
||||||
|
is_sandbox: boolean | null
|
||||||
|
original_transaction_id: string | null
|
||||||
|
product_id: string
|
||||||
|
purchased_at: string
|
||||||
|
revenuecat_app_user_id: string
|
||||||
|
status: Database["public"]["Enums"]["subscription_status"]
|
||||||
|
store: string | null
|
||||||
|
updated_at: string | null
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
cancel_reason?: string | null
|
||||||
|
created_at?: string | null
|
||||||
|
entitlement_ids?: string[] | null
|
||||||
|
expires_at?: string | null
|
||||||
|
grace_period_expires_at?: string | null
|
||||||
|
id?: string
|
||||||
|
is_sandbox?: boolean | null
|
||||||
|
original_transaction_id?: string | null
|
||||||
|
product_id: string
|
||||||
|
purchased_at: string
|
||||||
|
revenuecat_app_user_id: string
|
||||||
|
status?: Database["public"]["Enums"]["subscription_status"]
|
||||||
|
store?: string | null
|
||||||
|
updated_at?: string | null
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
cancel_reason?: string | null
|
||||||
|
created_at?: string | null
|
||||||
|
entitlement_ids?: string[] | null
|
||||||
|
expires_at?: string | null
|
||||||
|
grace_period_expires_at?: string | null
|
||||||
|
id?: string
|
||||||
|
is_sandbox?: boolean | null
|
||||||
|
original_transaction_id?: string | null
|
||||||
|
product_id?: string
|
||||||
|
purchased_at?: string
|
||||||
|
revenuecat_app_user_id?: string
|
||||||
|
status?: Database["public"]["Enums"]["subscription_status"]
|
||||||
|
store?: string | null
|
||||||
|
updated_at?: string | null
|
||||||
|
user_id?: string
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "subscriptions_user_id_fkey"
|
||||||
|
columns: ["user_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "profiles"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
topics: {
|
||||||
|
Row: {
|
||||||
|
created_at: string | null
|
||||||
|
description: string | null
|
||||||
|
icon: string | null
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
sort_order: number | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
created_at?: string | null
|
||||||
|
description?: string | null
|
||||||
|
icon?: string | null
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
sort_order?: number | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
created_at?: string | null
|
||||||
|
description?: string | null
|
||||||
|
icon?: string | null
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
sort_order?: number | null
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Views: {
|
Views: {
|
||||||
[_ in never]: never
|
[_ in never]: never
|
||||||
}
|
}
|
||||||
Functions: {
|
Functions: {
|
||||||
get_recommended_questions: {
|
cleanup_expired_videos: {
|
||||||
Args: {
|
Args: Record<PropertyKey, never>
|
||||||
target_pack_id?: string
|
Returns: {
|
||||||
|
deleted_count: number
|
||||||
|
deleted_paths: string[]
|
||||||
|
}[]
|
||||||
}
|
}
|
||||||
|
get_admin_role: {
|
||||||
|
Args: { check_user_id?: string }
|
||||||
|
Returns: Database["public"]["Enums"]["admin_role"]
|
||||||
|
}
|
||||||
|
get_answer_gap_status: {
|
||||||
|
Args: Record<PropertyKey, never>
|
||||||
|
Returns: {
|
||||||
|
is_blocked: boolean
|
||||||
|
threshold: number
|
||||||
|
unanswered_by_partner: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
get_auth_user_couple_id: { Args: Record<PropertyKey, never>; Returns: string }
|
||||||
|
get_pack_teaser_questions: {
|
||||||
|
Args: { target_pack_id: string }
|
||||||
Returns: {
|
Returns: {
|
||||||
id: string
|
id: string
|
||||||
|
intensity: number
|
||||||
text: string
|
text: string
|
||||||
partner_text: string | null
|
}[]
|
||||||
|
}
|
||||||
|
get_profiles_with_auth_info: {
|
||||||
|
Args: Record<PropertyKey, never>
|
||||||
|
Returns: {
|
||||||
|
avatar_url: string
|
||||||
|
couple_id: string
|
||||||
|
created_at: string
|
||||||
|
email: string
|
||||||
|
email_confirmed_at: string
|
||||||
|
id: string
|
||||||
|
is_premium: boolean
|
||||||
|
last_sign_in_at: string
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
get_recommended_questions: {
|
||||||
|
Args: { target_pack_id?: string }
|
||||||
|
Returns: {
|
||||||
|
allowed_couple_genders: string[]
|
||||||
|
id: string
|
||||||
|
intensity: number
|
||||||
is_two_part: boolean
|
is_two_part: boolean
|
||||||
pack_id: string
|
pack_id: string
|
||||||
intensity: number
|
|
||||||
partner_answered: boolean
|
partner_answered: boolean
|
||||||
|
partner_text: string
|
||||||
|
target_user_genders: string[]
|
||||||
|
text: string
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
get_user_media_files: {
|
||||||
|
Args: { user_id: string }
|
||||||
|
Returns: {
|
||||||
|
bucket_id: string
|
||||||
|
created_at: string
|
||||||
|
id: string
|
||||||
|
metadata: Json
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
get_user_storage_usage: {
|
||||||
|
Args: Record<PropertyKey, never>
|
||||||
|
Returns: {
|
||||||
|
owner: string
|
||||||
|
total_bytes: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
has_premium_access: { Args: { check_user_id: string }; Returns: boolean }
|
||||||
|
is_admin: { Args: { check_user_id?: string }; Returns: boolean }
|
||||||
|
is_super_admin: { Args: { check_user_id?: string }; Returns: boolean }
|
||||||
|
log_admin_action: {
|
||||||
|
Args: {
|
||||||
|
p_action: Database["public"]["Enums"]["audit_action"]
|
||||||
|
p_new_values?: Json
|
||||||
|
p_old_values?: Json
|
||||||
|
p_record_id: string
|
||||||
|
p_table_name: string
|
||||||
|
}
|
||||||
|
Returns: string
|
||||||
|
}
|
||||||
|
redeem_code_by_email: {
|
||||||
|
Args: { p_code: string; p_email: string }
|
||||||
|
Returns: Json
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Enums: {
|
Enums: {
|
||||||
|
admin_role: "pack_creator" | "super_admin"
|
||||||
answer_type: "yes" | "no" | "maybe"
|
answer_type: "yes" | "no" | "maybe"
|
||||||
|
audit_action: "INSERT" | "UPDATE" | "DELETE"
|
||||||
|
feedback_status:
|
||||||
|
| "new"
|
||||||
|
| "reviewed"
|
||||||
|
| "in_progress"
|
||||||
|
| "resolved"
|
||||||
|
| "closed"
|
||||||
|
feedback_type: "bug" | "feature_request" | "general" | "question"
|
||||||
match_type: "yes_yes" | "yes_maybe" | "maybe_maybe"
|
match_type: "yes_yes" | "yes_maybe" | "maybe_maybe"
|
||||||
|
subscription_status:
|
||||||
|
| "active"
|
||||||
|
| "cancelled"
|
||||||
|
| "expired"
|
||||||
|
| "billing_issue"
|
||||||
|
| "paused"
|
||||||
}
|
}
|
||||||
CompositeTypes: {
|
CompositeTypes: {
|
||||||
[_ in never]: never
|
[_ in never]: never
|
||||||
@@ -376,33 +889,33 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublicSchema = Database[Extract<keyof Database, "public">]
|
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
|
||||||
|
|
||||||
// Helper type to get schema keys excluding internal keys
|
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
|
||||||
type SchemaName = Exclude<keyof Database, "__InternalSupabase">
|
|
||||||
|
|
||||||
// Helper to safely get schema
|
|
||||||
type GetSchema<S extends SchemaName> = Database[S]
|
|
||||||
|
|
||||||
export type Tables<
|
export type Tables<
|
||||||
PublicTableNameOrOptions extends
|
DefaultSchemaTableNameOrOptions extends
|
||||||
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
|
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||||
| { schema: SchemaName },
|
| { schema: keyof DatabaseWithoutInternals },
|
||||||
TableName extends PublicTableNameOrOptions extends { schema: SchemaName }
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
? keyof (GetSchema<PublicTableNameOrOptions["schema"]>["Tables"] &
|
schema: keyof DatabaseWithoutInternals
|
||||||
GetSchema<PublicTableNameOrOptions["schema"]>["Views"])
|
}
|
||||||
|
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||||
|
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||||
: never = never,
|
: never = never,
|
||||||
> = PublicTableNameOrOptions extends { schema: SchemaName }
|
> = DefaultSchemaTableNameOrOptions extends {
|
||||||
? (GetSchema<PublicTableNameOrOptions["schema"]>["Tables"] &
|
schema: keyof DatabaseWithoutInternals
|
||||||
GetSchema<PublicTableNameOrOptions["schema"]>["Views"])[TableName] extends {
|
}
|
||||||
|
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||||
|
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||||
Row: infer R
|
Row: infer R
|
||||||
}
|
}
|
||||||
? R
|
? R
|
||||||
: never
|
: never
|
||||||
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
|
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
||||||
PublicSchema["Views"])
|
DefaultSchema["Views"])
|
||||||
? (PublicSchema["Tables"] &
|
? (DefaultSchema["Tables"] &
|
||||||
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
|
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||||
Row: infer R
|
Row: infer R
|
||||||
}
|
}
|
||||||
? R
|
? R
|
||||||
@@ -410,20 +923,24 @@ export type Tables<
|
|||||||
: never
|
: never
|
||||||
|
|
||||||
export type TablesInsert<
|
export type TablesInsert<
|
||||||
PublicTableNameOrOptions extends
|
DefaultSchemaTableNameOrOptions extends
|
||||||
| keyof PublicSchema["Tables"]
|
| keyof DefaultSchema["Tables"]
|
||||||
| { schema: SchemaName },
|
| { schema: keyof DatabaseWithoutInternals },
|
||||||
TableName extends PublicTableNameOrOptions extends { schema: SchemaName }
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
? keyof GetSchema<PublicTableNameOrOptions["schema"]>["Tables"]
|
schema: keyof DatabaseWithoutInternals
|
||||||
|
}
|
||||||
|
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||||
: never = never,
|
: never = never,
|
||||||
> = PublicTableNameOrOptions extends { schema: SchemaName }
|
> = DefaultSchemaTableNameOrOptions extends {
|
||||||
? GetSchema<PublicTableNameOrOptions["schema"]>["Tables"][TableName] extends {
|
schema: keyof DatabaseWithoutInternals
|
||||||
|
}
|
||||||
|
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||||
Insert: infer I
|
Insert: infer I
|
||||||
}
|
}
|
||||||
? I
|
? I
|
||||||
: never
|
: never
|
||||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||||
Insert: infer I
|
Insert: infer I
|
||||||
}
|
}
|
||||||
? I
|
? I
|
||||||
@@ -431,20 +948,24 @@ export type TablesInsert<
|
|||||||
: never
|
: never
|
||||||
|
|
||||||
export type TablesUpdate<
|
export type TablesUpdate<
|
||||||
PublicTableNameOrOptions extends
|
DefaultSchemaTableNameOrOptions extends
|
||||||
| keyof PublicSchema["Tables"]
|
| keyof DefaultSchema["Tables"]
|
||||||
| { schema: SchemaName },
|
| { schema: keyof DatabaseWithoutInternals },
|
||||||
TableName extends PublicTableNameOrOptions extends { schema: SchemaName }
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
? keyof GetSchema<PublicTableNameOrOptions["schema"]>["Tables"]
|
schema: keyof DatabaseWithoutInternals
|
||||||
|
}
|
||||||
|
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||||
: never = never,
|
: never = never,
|
||||||
> = PublicTableNameOrOptions extends { schema: SchemaName }
|
> = DefaultSchemaTableNameOrOptions extends {
|
||||||
? GetSchema<PublicTableNameOrOptions["schema"]>["Tables"][TableName] extends {
|
schema: keyof DatabaseWithoutInternals
|
||||||
|
}
|
||||||
|
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||||
Update: infer U
|
Update: infer U
|
||||||
}
|
}
|
||||||
? U
|
? U
|
||||||
: never
|
: never
|
||||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||||
Update: infer U
|
Update: infer U
|
||||||
}
|
}
|
||||||
? U
|
? U
|
||||||
@@ -452,38 +973,55 @@ export type TablesUpdate<
|
|||||||
: never
|
: never
|
||||||
|
|
||||||
export type Enums<
|
export type Enums<
|
||||||
PublicEnumNameOrOptions extends
|
DefaultSchemaEnumNameOrOptions extends
|
||||||
| keyof PublicSchema["Enums"]
|
| keyof DefaultSchema["Enums"]
|
||||||
| { schema: SchemaName },
|
| { schema: keyof DatabaseWithoutInternals },
|
||||||
EnumName extends PublicEnumNameOrOptions extends { schema: SchemaName }
|
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||||
? keyof GetSchema<PublicEnumNameOrOptions["schema"]>["Enums"]
|
schema: keyof DatabaseWithoutInternals
|
||||||
|
}
|
||||||
|
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||||
: never = never,
|
: never = never,
|
||||||
> = PublicEnumNameOrOptions extends { schema: SchemaName }
|
> = DefaultSchemaEnumNameOrOptions extends {
|
||||||
? GetSchema<PublicEnumNameOrOptions["schema"]>["Enums"][EnumName]
|
schema: keyof DatabaseWithoutInternals
|
||||||
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
|
}
|
||||||
? PublicSchema["Enums"][PublicEnumNameOrOptions]
|
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||||
|
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||||
|
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||||
: never
|
: never
|
||||||
|
|
||||||
export type CompositeTypes<
|
export type CompositeTypes<
|
||||||
PublicCompositeTypeNameOrOptions extends
|
PublicCompositeTypeNameOrOptions extends
|
||||||
| keyof PublicSchema["CompositeTypes"]
|
| keyof DefaultSchema["CompositeTypes"]
|
||||||
| { schema: SchemaName },
|
| { schema: keyof DatabaseWithoutInternals },
|
||||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||||
schema: SchemaName
|
schema: keyof DatabaseWithoutInternals
|
||||||
}
|
}
|
||||||
? keyof GetSchema<PublicCompositeTypeNameOrOptions["schema"]>["CompositeTypes"]
|
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||||
: never = never,
|
: never = never,
|
||||||
> = PublicCompositeTypeNameOrOptions extends { schema: SchemaName }
|
> = PublicCompositeTypeNameOrOptions extends {
|
||||||
? GetSchema<PublicCompositeTypeNameOrOptions["schema"]>["CompositeTypes"][CompositeTypeName]
|
schema: keyof DatabaseWithoutInternals
|
||||||
: PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"]
|
}
|
||||||
? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||||
|
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||||
|
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||||
: never
|
: never
|
||||||
|
|
||||||
export const Constants = {
|
export const Constants = {
|
||||||
public: {
|
public: {
|
||||||
Enums: {
|
Enums: {
|
||||||
|
admin_role: ["pack_creator", "super_admin"],
|
||||||
answer_type: ["yes", "no", "maybe"],
|
answer_type: ["yes", "no", "maybe"],
|
||||||
|
audit_action: ["INSERT", "UPDATE", "DELETE"],
|
||||||
|
feedback_status: ["new", "reviewed", "in_progress", "resolved", "closed"],
|
||||||
|
feedback_type: ["bug", "feature_request", "general", "question"],
|
||||||
match_type: ["yes_yes", "yes_maybe", "maybe_maybe"],
|
match_type: ["yes_yes", "yes_maybe", "maybe_maybe"],
|
||||||
|
subscription_status: [
|
||||||
|
"active",
|
||||||
|
"cancelled",
|
||||||
|
"expired",
|
||||||
|
"billing_issue",
|
||||||
|
"paused",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||||
|
import { createClient } from "jsr:@supabase/supabase-js@2";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup expired videos from storage.
|
||||||
|
* This function is called by pg_cron daily at 3 AM UTC.
|
||||||
|
* It finds videos where media_expires_at has passed, deletes the files from storage,
|
||||||
|
* and marks the messages as expired.
|
||||||
|
*/
|
||||||
|
Deno.serve(async (req) => {
|
||||||
|
// Only allow POST requests (from cron) or GET (for manual testing)
|
||||||
|
if (req.method !== "POST" && req.method !== "GET") {
|
||||||
|
return new Response("Method not allowed", { status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get("SUPABASE_URL")!,
|
||||||
|
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get expired videos that still have files
|
||||||
|
const { data: expiredVideos, error: fetchError } = await supabase
|
||||||
|
.from("messages")
|
||||||
|
.select("id, media_path")
|
||||||
|
.eq("media_type", "video")
|
||||||
|
.eq("media_expired", false)
|
||||||
|
.not("media_path", "is", null)
|
||||||
|
.lt("media_expires_at", new Date().toISOString());
|
||||||
|
|
||||||
|
if (fetchError) {
|
||||||
|
console.error("Error fetching expired videos:", fetchError);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: fetchError.message }),
|
||||||
|
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expiredVideos || expiredVideos.length === 0) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ deleted: 0, message: "No expired videos found" }),
|
||||||
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete files from storage
|
||||||
|
const paths = expiredVideos.map((v) => v.media_path).filter(Boolean) as string[];
|
||||||
|
|
||||||
|
if (paths.length > 0) {
|
||||||
|
const { error: storageError } = await supabase.storage
|
||||||
|
.from("chat-media")
|
||||||
|
.remove(paths);
|
||||||
|
|
||||||
|
if (storageError) {
|
||||||
|
console.error("Error deleting from storage:", storageError);
|
||||||
|
// Continue anyway to mark as expired - files may already be deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark messages as expired
|
||||||
|
const ids = expiredVideos.map((v) => v.id);
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from("messages")
|
||||||
|
.update({ media_expired: true, media_path: null })
|
||||||
|
.in("id", ids);
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
console.error("Error marking messages as expired:", updateError);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: updateError.message }),
|
||||||
|
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Cleaned up ${expiredVideos.length} expired videos`);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
deleted: expiredVideos.length,
|
||||||
|
paths: paths,
|
||||||
|
}),
|
||||||
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Unexpected error:", err);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: "Internal server error" }),
|
||||||
|
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
-- Add video support to chat messages with 30-day retention policy
|
||||||
|
-- Videos expire 30 days after being viewed by the recipient
|
||||||
|
|
||||||
|
-- Add media type to distinguish images from videos
|
||||||
|
ALTER TABLE messages ADD COLUMN media_type TEXT CHECK (media_type IN ('image', 'video'));
|
||||||
|
|
||||||
|
-- Track when media expires (set when video is viewed: media_viewed_at + 30 days)
|
||||||
|
ALTER TABLE messages ADD COLUMN media_expires_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Track if media was deleted due to expiration
|
||||||
|
ALTER TABLE messages ADD COLUMN media_expired BOOLEAN DEFAULT false;
|
||||||
|
|
||||||
|
-- Backfill existing media as images
|
||||||
|
UPDATE messages SET media_type = 'image' WHERE media_path IS NOT NULL;
|
||||||
|
|
||||||
|
-- Enable pg_cron extension for scheduled cleanup
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_cron WITH SCHEMA extensions;
|
||||||
|
|
||||||
|
-- Create cleanup function that handles both DB updates and storage deletion tracking
|
||||||
|
CREATE OR REPLACE FUNCTION cleanup_expired_videos()
|
||||||
|
RETURNS TABLE(deleted_count int, deleted_paths text[]) AS $$
|
||||||
|
DECLARE
|
||||||
|
expired_paths text[];
|
||||||
|
count int;
|
||||||
|
BEGIN
|
||||||
|
-- Get paths of expired videos
|
||||||
|
SELECT array_agg(media_path)
|
||||||
|
INTO expired_paths
|
||||||
|
FROM messages
|
||||||
|
WHERE media_type = 'video'
|
||||||
|
AND media_expires_at IS NOT NULL
|
||||||
|
AND media_expires_at < NOW()
|
||||||
|
AND media_expired = false
|
||||||
|
AND media_path IS NOT NULL;
|
||||||
|
|
||||||
|
-- Mark as expired and clear media_path
|
||||||
|
WITH updated AS (
|
||||||
|
UPDATE messages
|
||||||
|
SET media_expired = true, media_path = NULL
|
||||||
|
WHERE media_type = 'video'
|
||||||
|
AND media_expires_at IS NOT NULL
|
||||||
|
AND media_expires_at < NOW()
|
||||||
|
AND media_expired = false
|
||||||
|
AND media_path IS NOT NULL
|
||||||
|
RETURNING 1
|
||||||
|
)
|
||||||
|
SELECT count(*) INTO count FROM updated;
|
||||||
|
|
||||||
|
RETURN QUERY SELECT count, COALESCE(expired_paths, ARRAY[]::text[]);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- Schedule daily cleanup at 3 AM UTC
|
||||||
|
-- The database function marks videos as expired, edge function handles storage deletion
|
||||||
|
SELECT cron.schedule(
|
||||||
|
'cleanup-expired-videos',
|
||||||
|
'0 3 * * *',
|
||||||
|
'SELECT * FROM cleanup_expired_videos()'
|
||||||
|
);
|
||||||
Generated
+32
@@ -80,6 +80,7 @@
|
|||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-apple-authentication": "~7.1.3",
|
"expo-apple-authentication": "~7.1.3",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
|
"expo-av": "~15.0.2",
|
||||||
"expo-blur": "~14.0.3",
|
"expo-blur": "~14.0.3",
|
||||||
"expo-build-properties": "^0.13.3",
|
"expo-build-properties": "^0.13.3",
|
||||||
"expo-clipboard": "~7.0.1",
|
"expo-clipboard": "~7.0.1",
|
||||||
@@ -101,6 +102,7 @@
|
|||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
|
"react-native-compressor": "^1.13.0",
|
||||||
"react-native-gesture-handler": "~2.20.0",
|
"react-native-gesture-handler": "~2.20.0",
|
||||||
"react-native-purchases": "^9.6.12",
|
"react-native-purchases": "^9.6.12",
|
||||||
"react-native-reanimated": "~3.16.0",
|
"react-native-reanimated": "~3.16.0",
|
||||||
@@ -10140,6 +10142,23 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-av": {
|
||||||
|
"version": "15.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-av/-/expo-av-15.0.2.tgz",
|
||||||
|
"integrity": "sha512-AHIHXdqLgK1dfHZF0JzX3YSVySGMrWn9QtPzaVjw54FAzvXfMt4sIoq4qRL/9XWCP9+ICcCs/u3EcvmxQjrfcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-web": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-web": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-blur": {
|
"node_modules/expo-blur": {
|
||||||
"version": "14.0.3",
|
"version": "14.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.3.tgz",
|
||||||
@@ -14730,6 +14749,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-compressor": {
|
||||||
|
"version": "1.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-compressor/-/react-native-compressor-1.13.0.tgz",
|
||||||
|
"integrity": "sha512-vJYbrcjz2S7kgE3Q7444F71CjEDq5Qg6sGn67N9fJ0I1K6EhB/ZxpULdDe3FxJ4h/ncJc0oxOzNWZhxtGTqcQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-gesture-handler": {
|
"node_modules/react-native-gesture-handler": {
|
||||||
"version": "2.20.2",
|
"version": "2.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
||||||
|
|||||||
Reference in new issue
Block a user