From fa772dd919b34e915615eb34562e0d69b1dd6588 Mon Sep 17 00:00:00 2001 From: Avom Brice Date: Sat, 23 Aug 2025 03:03:15 +0100 Subject: [PATCH] feat: redesign subscription modal with glass morphism design and single Pro Plan focus - Update subscription modal to show only one Pro Plan (like home page pricing) - Implement glass morphism design matching home page styling - Add neon accents with consistent color scheme (#38f9d7, #a78bfa) - Include Pro Plan glow effects and diamond icon - Use same features list as home page Pro Plan - Improve Stripe product sync and error handling - Add help page with comprehensive app usage guide - Update header spacing and organization - Fix collaborator limit logic in settings form - Enhance middleware and authentication flow - Replace console.log with custom logger throughout - Update documentation and troubleshooting guides --- .gitignore | 3 +- TROUBLESHOOTING.md | 765 +++++++++++------- public/images/realtime_wp.png | Bin 0 -> 885737 bytes src/app/(main)/dashboard/layout.tsx | 47 ++ src/app/(site)/page.tsx | 4 +- src/app/help/page.tsx | 560 +++++++++++++ .../features/landing-page/api/data.ts | 7 +- .../landing-page/components/footer.tsx | 4 +- .../landing-page/components/header.tsx | 249 +++--- .../features/landing-page/landing.page.tsx | 2 +- .../features/settings/settings-form.tsx | 80 +- .../global-components/subscription-modal.tsx | 99 ++- src/lib/stripe/admin-tasks.ts | 31 +- src/lib/supabase/queries.ts | 77 +- src/middleware.ts | 53 +- src/utils/sync-stripe-products.ts | 77 +- 16 files changed, 1453 insertions(+), 605 deletions(-) create mode 100644 public/images/realtime_wp.png create mode 100644 src/app/help/page.tsx diff --git a/.gitignore b/.gitignore index 9aab6a2..29389f6 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ scripts/ tsconfig.tsbuildinfo AUTH_QUICK_REFERENCE.md AUTHENTICATION_TROUBLESHOOTING.md -src/app/api/debug.ts \ No newline at end of file +src/app/api/debug.ts +src/app/api/debug \ No newline at end of file diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index cee98df..ab1a5c5 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -1,387 +1,554 @@ -# Real-Time Collaborative Platform Troubleshooting Guide +# πŸš€ Real-Time Collaborative Platform - Troubleshooting & Setup Guide -This document contains solutions for common issues encountered when setting up and running the real-time collaborative platform. +## πŸ“‹ Table of Contents -## πŸ” Authentication Issues +- [Project Overview](#project-overview) +- [Development Environment Setup](#development-environment-setup) +- [Docker Compose Setup](#docker-compose-setup) +- [Supabase Configuration](#supabase-configuration) +- [Authentication System](#authentication-system) +- [Common Issues & Solutions](#common-issues--solutions) +- [Production Deployment](#production-deployment) +- [Performance Optimization](#performance-optimization) -### Issue 1: "user from dashboard page: null" +--- -**Problem**: User is not detected after successful login, dashboard shows null user. +## 🎯 Project Overview -**Root Cause**: Server-side Supabase client was not properly configured for authentication. +**Real-Time Collaborative Platform** is a modern web application built with Next.js 15, featuring: -**Solution**: +- **Real-time collaboration** with live document editing +- **Workspace management** for teams and projects +- **User authentication** via Supabase Auth +- **Subscription management** with Stripe integration +- **Collaborator management** with role-based access +- **Cross-platform compatibility** (web, mobile-responsive) -1. **Update server-side client** (`src/utils/server.ts`): - - ```typescript - import { createServerClient, type CookieOptions } from '@supabase/ssr'; - import { cookies } from 'next/headers'; - - export async function createClient() { - const cookieStore = await cookies(); - return createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - get(name: string) { - return cookieStore.get(name)?.value; - }, - set(name: string, value: string, options: CookieOptions) { - try { - cookieStore.set({ name, value, ...options }); - } catch (error) { - // Server Component context - } - }, - remove(name: string, options: CookieOptions) { - try { - cookieStore.set({ name, value: '', ...options }); - } catch (error) { - // Server Component context - } - }, - }, - } - ); - } - ``` - -2. **Update auth actions** (`src/lib/server-action/auth-action.ts`): - - ```typescript - import { createClient } from '@/utils/server'; - - export async function actionLoginUser({ email, password }) { - const supabase = await createClient(); - const response = await supabase.auth.signInWithPassword({ - email, - password, - }); - return response; - } - ``` - -3. **Update dashboard page** (`src/components/features/main/dashboard/dashboard-page.tsx`): - - ```typescript - import { createClient } from '@/utils/server'; - - const DashboardPage = async () => { - const supabase = await createClient(); - const { - data: { user }, - } = await supabase.auth.getUser(); - // ... rest of the code - }; - ``` - -### Issue 2: No redirect to dashboard after login - -**Problem**: User successfully logs in but doesn't get redirected to dashboard. - -**Root Cause**: Middleware not properly detecting sessions or auth state not updating. +### πŸ—οΈ Tech Stack -**Solution**: +- **Frontend**: Next.js 15, React 18, TypeScript +- **Backend**: Next.js API Routes, Supabase +- **Database**: PostgreSQL (via Supabase) +- **Authentication**: Supabase Auth with OAuth & Email/Password +- **Real-time**: WebSocket connections +- **Styling**: Tailwind CSS, shadcn/ui components +- **State Management**: React Context, Zustand +- **Payment**: Stripe integration +- **Deployment**: Vercel, Docker -1. **Add auth state listener** (`src/lib/providers/supabase-user-provider.tsx`): - - ```typescript - useEffect(() => { - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange(async (event, session) => { - if (session?.user) { - setUser(session.user); - } else { - setUser(null); - } - }); - return () => subscription.unsubscribe(); - }, [supabase]); - ``` - -2. **Update middleware** (`src/middleware.ts`): - - ```typescript - export async function middleware(req: NextRequest) { - const supabase = await createClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (req.nextUrl.pathname.startsWith('/dashboard')) { - if (!session) { - return NextResponse.redirect(new URL('/login', req.url)); - } - } - - if (['/login', '/signup'].includes(req.nextUrl.pathname)) { - if (session) { - return NextResponse.redirect(new URL('/dashboard', req.url)); - } - } - return NextResponse.next(); - } - ``` - -## πŸ—„οΈ Database Issues - -### Issue 3: "relation 'products' does not exist" (and other tables) - -**Problem**: Database tables missing, causing build errors and runtime failures. - -**Root Cause**: Database schema not properly migrated or tables not created. +--- -**Solution**: +## πŸ› οΈ Development Environment Setup -1. **Check existing tables**: - - ```bash - psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "\dt" - ``` - -2. **Create missing tables manually**: - - ```sql - -- Create enum types - DO $$ BEGIN - CREATE TYPE "subscription_status" AS ENUM ('trialing', 'active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid'); - EXCEPTION WHEN duplicate_object THEN null; END $$; - - DO $$ BEGIN - CREATE TYPE "pricing_plan_interval" AS ENUM ('day', 'week', 'month', 'year'); - EXCEPTION WHEN duplicate_object THEN null; END $$; - - DO $$ BEGIN - CREATE TYPE "pricing_type" AS ENUM ('one_time', 'recurring'); - EXCEPTION WHEN duplicate_object THEN null; END $$; - - -- Create missing tables - CREATE TABLE IF NOT EXISTS "users" ( - "id" uuid PRIMARY KEY NOT NULL, - "full_name" text, - "avatar_url" text, - "billing_address" jsonb, - "payment_method" jsonb, - "email" text, - "updated_at" timestamp with time zone - ); - - CREATE TABLE IF NOT EXISTS "workspaces" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "workspaces_owner" uuid NOT NULL, - "title" text NOT NULL, - "icon_id" text NOT NULL, - "data" text NOT NULL, - "in_trash" text, - "logo" text, - "banner_url" text - ); - - -- Add other missing tables... - ``` - -3. **Fix database configuration** (`src/lib/supabase/db.ts`): - ```typescript - // Use correct database URL, not API URL - const client = postgres(process.env.NEXT_PUBLIC_DATABASE_URL as string, { max: 1 }); - ``` - -### Issue 4: JWT Secret Mismatch - -**Problem**: Authentication fails due to JWT secret mismatch between Supabase CLI and environment. +### Prerequisites -**Solution**: +- Node.js 18+ +- Docker & Docker Compose +- Git +- pnpm (recommended) or yarn -1. **Check Supabase CLI JWT secret**: +### Initial Setup - ```bash - supabase status - ``` +```bash +# Clone the repository +git clone +cd real-time-collaborative-plateform -2. **Update .env to match**: - ```env - NEXT_PUBLIC_JWT_SECRET="super-secret-jwt-token-with-at-least-32-characters-long" - ``` +# Install dependencies +pnpm install -## πŸ”§ Build Issues +# Copy environment variables +cp .env.example .env.local -### Issue 5: Build errors with missing dependencies +# Start development server +pnpm dev +``` -**Problem**: `Cannot find package '@next/bundle-analyzer'` or similar. +--- -**Solution**: +## 🐳 Docker Compose Setup -```bash -yarn add @next/bundle-analyzer -``` +### Local Development with Docker -### Issue 6: UUID import errors +Our project includes a comprehensive Docker setup for local development: -**Problem**: `uuidv4` import issues in dashboard setup. +#### 1. **Main Application Container** -**Solution**: +```yaml +# docker-compose.yml +services: + app: + build: . + ports: + - '3000:3000' + environment: + - NODE_ENV=development + volumes: + - .:/app + - /app/node_modules + depends_on: + - supabase +``` -```typescript -// Change from: -import { uuid } from 'uuidv4'; +#### 2. **Supabase Local Development** + +```yaml +supabase: + image: supabase/supabase-dev + ports: + - '54321:54321' # Supabase API + - '54322:54322' # PostgreSQL + - '54323:54323' # Studio + environment: + - POSTGRES_PASSWORD=your_password + - JWT_SECRET=your_jwt_secret + volumes: + - supabase_data:/var/lib/postgresql/data +``` + +#### 3. **Redis for Caching** -// To: -import { v4 as uuid } from 'uuid'; +```yaml +redis: + image: redis:alpine + ports: + - '6379:6379' + volumes: + - redis_data:/data ``` -## πŸš€ Setup Guide +### Running with Docker -### Prerequisites +```bash +# Start all services +docker-compose up -d -1. **Install Supabase CLI**: +# View logs +docker-compose logs -f app - ```bash - npm install -g supabase - ``` +# Stop services +docker-compose down + +# Rebuild and restart +docker-compose up -d --build +``` -2. **Install Docker Desktop** and ensure it's running +--- -### Step 1: Start Supabase Services +## πŸ” Supabase Configuration + +### Local Development Setup + +#### 1. **Supabase CLI Installation** ```bash -# Navigate to your project directory -cd real-time-collaborative-plateform +# Install Supabase CLI +npm install -g supabase -# Start Supabase services -supabase start +# Login to Supabase +supabase login + +# Initialize project +supabase init ``` -### Step 2: Configure Environment Variables +#### 2. **Local Supabase Start** + +```bash +# Start local Supabase +supabase start + +# This will output: +# API URL: http://127.0.0.1:54321 +# DB URL: postgresql://postgres:postgres@127.0.0.1:54321:54322/postgres +# Studio URL: http://127.0.0.1:54323 +# Inbucket URL: http://127.0.0.1:54324 +# JWT secret: super-secret-jwt-token-with-at-least-32-characters-long +# anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +# service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` -Create `.env` file with correct values: +#### 3. **Environment Variables for Local Development** -```env -# Supabase CLI Configuration +```bash +# .env.local NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 -NEXT_PUBLIC_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... -NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJ... -NEXT_PUBLIC_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_local_anon_key +SUPABASE_SERVICE_ROLE_KEY=your_local_service_role_key NEXT_PUBLIC_SITE_URL=http://localhost:3000 ``` -### Step 3: Install Dependencies +### Production Supabase Setup + +#### 1. **Create Production Project** + +- Go to [supabase.com](https://supabase.com) +- Create new project +- Note down project URL and API keys + +#### 2. **Production Environment Variables** ```bash -yarn install +# .env.production +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_production_anon_key +SUPABASE_SERVICE_ROLE_KEY=your_production_service_role_key +NEXT_PUBLIC_SITE_URL=https://your-domain.com ``` -### Step 4: Start Development Server +#### 3. **Database Schema Migration** ```bash -yarn dev +# Generate migration from local changes +supabase db diff --schema public + +# Apply migrations to production +supabase db push --db-url "postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres" +``` + +--- + +## πŸ”‘ Authentication System + +### Authentication Flow + +#### 1. **User Registration** + +- **Email/Password**: Traditional signup with email verification +- **OAuth**: Google, GitHub integration +- **Profile Creation**: Automatic user profile creation in `users` table + +#### 2. **Login Process** + +- **Session Management**: JWT-based authentication +- **Middleware Protection**: Route-level authentication checks +- **Redirect Logic**: Authenticated users redirected to dashboard + +#### 3. **Logout Process** + +- **Session Invalidation**: Multiple logout attempts for reliability +- **State Cleanup**: Local storage, cookies, and IndexedDB clearing +- **Redirect**: Dedicated logout page for final cleanup + +### Authentication Components + +#### **Server Actions** (`src/lib/server-action/auth-action.ts`) + +```typescript +// User login +export async function actionLoginUser(formData: FormData) { + // Email/password authentication + // User profile creation + // Redirect to dashboard +} + +// OAuth login +export async function socialLogin(provider: 'google' | 'github') { + // OAuth flow initiation + // Redirect to provider +} + +// User logout +export async function actionLogoutUser() { + // Session cleanup + // Redirect to home +} ``` -## πŸ› Debugging +#### **API Routes** (`src/app/api/auth/callback/route.ts`) + +```typescript +// OAuth callback handling +export async function GET(request: NextRequest) { + // Exchange code for session + // Create user profile + // Redirect to dashboard +} +``` + +#### **Middleware** (`src/middleware.ts`) + +```typescript +// Route protection +export async function middleware(req: NextRequest) { + // Check authentication status + // Redirect unauthenticated users + // Handle public/protected routes +} +``` + +--- + +## 🚨 Common Issues & Solutions + +### 1. **Authentication Issues** + +#### **Problem**: OAuth redirects to `/login?error=auth_failed` + +**Solution**: + +- Check Supabase OAuth configuration +- Verify redirect URLs in Supabase dashboard +- Ensure environment variables are correct + +#### **Problem**: Session persists after logout + +**Solution**: + +- Use dedicated `/logout` page for final cleanup +- Clear all browser storage (localStorage, sessionStorage, cookies) +- Implement aggressive logout with multiple attempts + +#### **Problem**: Middleware redirects to wrong page + +**Solution**: + +- Check route configuration in `middleware.ts` +- Verify public/protected route definitions +- Ensure proper session validation + +### 2. **Database Issues** + +#### **Problem**: User profile not created after signup + +**Solution**: + +- Check `ensureUserProfile` function in `auth-utils.ts` +- Verify database permissions +- Check Supabase RLS policies + +#### **Problem**: Collaborator limit not enforced + +**Solution**: + +- Verify subscription status checking +- Check collaborator count logic +- Ensure proper plan validation + +### 3. **Build & Development Issues** + +#### **Problem**: `Module not found` errors -### Check Authentication Flow +**Solution**: + +- Clear Next.js cache: `rm -rf .next` +- Reinstall dependencies: `pnpm install` +- Check import paths and file structure + +#### **Problem**: Hydration mismatch warnings + +**Solution**: -1. **Server logs**: Look for auth action debug output -2. **Browser console**: Check for user provider logs -3. **Network tab**: Verify cookies are being set -4. **Middleware logs**: Check session detection +- Use `suppressHydrationWarning` for dynamic content +- Ensure consistent server/client rendering +- Check theme switching components -### Check Database Connection +### 4. **Docker Issues** + +#### **Problem**: Port conflicts + +**Solution**: ```bash -# Test database connection -psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "SELECT version();" +# Check running containers +docker ps + +# Stop conflicting services +docker-compose down -# List all tables -psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "\dt" +# Use different ports +docker-compose up -d -p 3001:3000 ``` -### Check Supabase Services +#### **Problem**: Volume mounting issues + +**Solution**: ```bash -# Check service status -supabase status +# Rebuild containers +docker-compose down +docker-compose up -d --build -# View logs -supabase logs +# Check volume permissions +docker volume ls +docker volume inspect ``` -## πŸ“‹ Common Commands +--- + +## πŸš€ Production Deployment -### Database Operations +### Vercel Deployment + +#### 1. **Environment Variables** + +- Set all production environment variables in Vercel dashboard +- Ensure `NODE_ENV=production` +- Configure Supabase production URLs + +#### 2. **Build Configuration** + +```json +// vercel.json +{ + "buildCommand": "pnpm build", + "outputDirectory": ".next", + "installCommand": "pnpm install", + "framework": "nextjs" +} +``` + +#### 3. **Domain Configuration** + +- Configure custom domain in Vercel +- Update Supabase redirect URLs +- Set up SSL certificates + +### Docker Production + +#### 1. **Production Dockerfile** + +```dockerfile +# Multi-stage build +FROM node:18-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production + +FROM node:18-alpine AS runner +WORKDIR /app +COPY --from=builder /app ./ +COPY . . +EXPOSE 3000 +CMD ["npm", "start"] +``` + +#### 2. **Production Compose** + +```yaml +# docker-compose.prod.yml +version: '3.8' +services: + app: + build: . + ports: + - '3000:3000' + environment: + - NODE_ENV=production + restart: unless-stopped +``` + +--- + +## ⚑ Performance Optimization + +### 1. **Code Splitting** + +- Use dynamic imports for heavy components +- Implement route-based code splitting +- Lazy load non-critical features + +### 2. **Caching Strategy** + +- Implement Redis caching for database queries +- Use Next.js built-in caching +- Optimize image loading with Next.js Image + +### 3. **Database Optimization** + +- Add proper indexes to frequently queried columns +- Implement connection pooling +- Use database views for complex queries + +### 4. **Bundle Optimization** ```bash -# Connect to database -psql postgresql://postgres:postgres@127.0.0.1:54322/postgres +# Analyze bundle size +pnpm build +# Check .next/analyze for bundle analysis + +# Optimize imports +pnpm add @next/bundle-analyzer +``` -# Reset database (if needed) -supabase db reset +--- -# Generate new migration -npx drizzle-kit generate +## πŸ”§ Development Tools -# Push schema changes -npx drizzle-kit push +### 1. **Custom Logger** + +```typescript +// src/utils/logger.ts +import { logger } from '@/utils/logger'; + +// Development-only logging +logger.info('User action', { userId, action }); +logger.error('Error occurred', error); +logger.warn('Warning message', { context }); ``` -### Development +### 2. **Debug Mode** ```bash -# Start development server -yarn dev +# Enable debug bypass in middleware +?debug=bypass -# Build for production -yarn build +# Development environment variables +NODE_ENV=development +DEBUG=true +``` + +### 3. **Database Tools** + +```bash +# Supabase Studio (local) +http://localhost:54323 -# Run linting -yarn lint +# Database connection +psql postgresql://postgres:postgres@localhost:54322/postgres + +# Generate types +supabase gen types typescript --local > types/supabase.ts ``` -## Verification Checklist +--- + +## πŸ“š Additional Resources + +### Documentation + +- [Next.js Documentation](https://nextjs.org/docs) +- [Supabase Documentation](https://supabase.com/docs) +- [Tailwind CSS Documentation](https://tailwindcss.com/docs) +- [shadcn/ui Components](https://ui.shadcn.com/) -- [ ] Supabase CLI is running (`supabase status`) -- [ ] All database tables exist (`\dt` command) -- [ ] Environment variables are correct -- [ ] JWT secret matches Supabase CLI -- [ ] Server-side client is properly configured -- [ ] Auth actions use server-side client -- [ ] User provider has auth state listener -- [ ] Middleware is properly configured -- [ ] No build errors (`yarn dev` runs successfully) -- [ ] Login redirects to dashboard -- [ ] Dashboard loads without errors +### Community -## Still Having Issues? +- [Next.js Discord](https://discord.gg/nextjs) +- [Supabase Discord](https://discord.supabase.com) +- [GitHub Issues](https://github.com/your-repo/issues) -1. **Check the logs**: Look at server console and browser console -2. **Verify environment**: Ensure all environment variables are set correctly -3. **Test step by step**: Try each part of the auth flow individually -4. **Compare with working setup**: Use this troubleshooting guide as reference +### Support -## πŸ“ Recent Fixes Applied +- **Email**: support@yourcompany.com +- **Documentation**: `/help` page in the app +- **GitHub**: Create issues for bugs and feature requests -### Authentication Fixes (Latest) +--- -- Fixed server-side Supabase client configuration -- Updated auth actions to use server-side client -- Added proper session handling in user provider -- Fixed middleware configuration and routing -- Added debugging logs for authentication flow +## πŸŽ‰ Getting Help -### Database Fixes (Latest) +If you encounter issues not covered in this guide: -- Updated database configuration to use correct URL -- Created missing database tables and enum types -- Fixed foreign key relationships -- Resolved JWT secret mismatch +1. **Check the logs** using our custom logger +2. **Search existing issues** on GitHub +3. **Create a new issue** with detailed information +4. **Join our community** for real-time support +5. **Review the Help page** at `/help` in the application -### Build Fixes (Latest) +--- -- Fixed UUID import in dashboard setup -- Resolved merge conflicts in middleware -- Added missing dependencies -- Fixed file structure issues +_Last updated: august 2025_ +_author: Avom brice_ +_check my portfolio: https://maebrieporfolio.vercel.app/_ +_Version: 2.0.0_ diff --git a/public/images/realtime_wp.png b/public/images/realtime_wp.png new file mode 100644 index 0000000000000000000000000000000000000000..5b091bbeb5497ee2f3ba5ea673f74aa1ca3bcc40 GIT binary patch literal 885737 zcmeFZ2UJtfw?7&Py{MGXMLHx9qy&(rfT7ofBtYmr2}P+Yiqbnu3qlZt03jryNC}FH z6hWmE2nc*t>_}0*QdIb=kMAw(-uu?N@4mPG_xILY_x!0PY!Km2_d&XECtQ@a2Fu;}07f+_)ko=gBh zYU$tNRB8c$qZt4IbL!vX{!J$517idKsW`5~D`!|30PwX501);90K}#N00HlRN;|y# zC%h>fCdnQ0b?)#82Sfrw0SW+nKr|p2pnXW`0dxSm0K?zQfU}3q`M(zaF64i$gu|zQ z_X9-uxDy2pfE*$KP7w~E2*>ZofD?aTPw7Z5zmkbms3MfiBR zIl1|TMFjZxIixuNoQGLN6m@xcEfImKGkl^k#am)Q{7SKP1L7bFJ@=A3bFT#?4XuzU zv}ZzT+2Eg2GRpce158{z@n%Iw7i;0Uv`R3U+BwUff1`TJ_$)l+Vz-*QhLK0&g|u>J zkF{55ddA&h8{7KKA#ivj#KbP_QUm4j*F$FH5BcQa{#z9P$}E7J*^Lq&(1U!_2 z2v7uY2Jq*XLC)WR@l8j+lYh>sygtgNO^QeY(gzie6b^{~H2+n0X3v9FVy}90$AtCj zz*XgbRTEfw=s`x4>vR7fT9kv|0A>93-bZ2s4#zEzL`enPQ`GCi`CrwNjfNe1dbLkk zy#5L0k@;_|5aB#e{@GQ$}TZE$UqSSg_ zQs3)Sv(r_*tz{nt3mh_D9qT`J?W2eczW-Z}Jm3vb&aDwt9%Y0<`q*fcgC_iR71IxL zPo<6&T-SLEKLT4xAt@+WMC$${*#B>p#sBoeIIz4xbK*(1aNE0%QxY0qyiaS1sagJ| zEAl?tEL^W$EX?jyuMXc4myxrVIABrLUI90Z3Su~l#MEB#b6wfyHJQ1{rOaz)Csg=a zm6ivzTH_tq#=^pGXmo&EqR^Xhn)ARw_-s)_L$(_v$`-XcI&06L&B-+Z0-0F#QR2>a zat{-gQD}_2s?`)y_lTyQdn@To_Jx>8d(-z=SLUyimUW;6PpF!vZYYY~IiZ786tmb( zFF4Q93>*f$5cJU&%W}Q**KLO#-@=DoA|KS=i*E7 zetF36M~zSJE|HhlL|%A#he=)P{{oUasd=kx1#qofhx=<;3fGDK#h)7LzX9aQvRm|< zJ2p3dZ9j|lM7rPSx+Gcj=*u6seXk>K{2XQ+gkz@&;n$zKkv*((ImLu+a{06BZ31iG zI_dNSoneQQ!_N34{iElVivD{C=6~#rf}xj+3fAm~BkN%t5FKB%*D(Nxxu~_uo1C|q zQzAAOqpPGnf5^> z?UT+-tEc9F_y?$s->lhqFqB{2pHgY?xh767@}hTst!^p@EIQ@eqT~;XP7FufMQhE^ z^mT3Wo&-%s?xo9YQ#XfoL|qj&GNT}D)?ADu_BIOwvcq+#2pgv5bwaxc2VncqfSm&C z;T=M_@`i`Cm+=RwP1pYHMbElQe+9Ll#wDV*Obra_2Ds@9vJ3>O|ABAR1} z0V82=41DLnm(LD$9$)zrZdCZ7<$U-VKR*1ANo>Mt)jQTz>6gX7z06Irw8h4w8SPIZ zlyzlT!b2(V?1k+z^~6)olU*_u+s$PxIL_alEj%XX3Y#r7n|>m$&8vP5^q8TmQMM3I za~Ha4Wp3lv$LeY}#2HWC`QxO>cOpI{iw%P;_f2vA>+`Yz^YJPM5G9F;~(xSnhz$ruaY5?vmrsjMRwi9h{$M z$xTfloaZ~^{Ad>=AF#U9)IqXOc+J0=C$Od(CfWg3P0M8M5}lL>ydygB$XhQ@ie;S7 zUVF4tdu^Zb$>HR;8)wV^S=RiyPllLe3GyFqvrDY ze}K~W9|jN~2glBN1bbe z+)3frhFy@IS@=h|GmH@bCu~OY?u^-=fb;xM_{^fFk|fXN82|O@W4iiYc=zqCMvvQb zi~27!m*4PybTyMc(`nB54akH%_(geEv!ng$*cp>cE6;vbZifE_Mt|Hjboy48_&xQ< z>=cjd##F?a*A8uGnmfWD3idocI$3j3?^@VpuipRx^rfH9rRJ-?eF|6Ow+BiO^p~c- zBk5CXk(FPpHva@}H`NH;UaXnFHc0fl<{t7-ES;z}+Y0$LzSsFnoVLARrFMDx{uPV= z&E|ijG%cy#0au=7y2yG6Cw?B@9q`NpWekLC!^EkfF|F;*nue8)7CG-~M?D(Gc~r&s zPWM&_iJ_1T-J5JNEx91>hEZIJ95ah%u9^6Xf4U|+ft=S<@MeEI`PtDHU!lm#@Sa^s0Z@uXsvcMcgK?(Uv zC?vXJCz-rpakqCy*Egn&sAMn_YoS|#+q2;qAv-RgOZnU=mc3&{3dpgVe@)8L@V<3x zmez9l4CmABw#-Jyn&mGa@66Gyt}BttDu%feOWs<>bB;l#nnEs7o#B)Px zP3HE-Z-Z~*hBjw_=(wg%RTtwtV{Py~`qD9>P>!lX(YuVPGt~Bbx;0vZV zIIos5>84jX&ECSG;X&d-z00&-z83_>Eqn@2t}=nf^7U~kI(u=adGge25y^6#Ir`DV z^ruR9|Lc1GPgChXT#Wy<_VxdL^_TN|;B21bS(Nz08(r7K_wSi{vpEGuo4*Zn6tu(7 z3yg9tXG<^LzTdlY{9}$_iq!&M?kMfO0am@qA@q)=5OUJJ?0wRsKuetA5|lVMAwgS9 z*nMBxGpogr1B;$GZ!ryYGPIEjkO|Vi;w}uEjtPo-yc`|%auHR6bg&Pw(lv`uv06z~ z_$D}-XYZ(l#StL8_fuPS)jFob zdxzWHw6W|`K0<+R;kmId5uAW7APnjs^@t#4nu^$m`Wn;h7a<$zZYuVDv^WC=k}REz zcl_7$fDyT1W-4=e|czx6woTqJEgz?UNVsb{AqDp_KOG<%3Z`X7Q8J}zY!US7Oe zwEpvc>8l@d%TTg;xIt z{3x3KIsp0-@r7re_;164Pg77}Ek_IrRkva=S_^ib%}a<%Wpaw!O^B7KIuI2{>TL>! zEL_aCCxOlYkwg5KbHUbATo~LR1c`u`$m;-|8Y1Jai%0_i+I%^M{T9Bsl6t3t$4`+` zMA5SrJ9vaV0ge|sC6E6gACTK5bNgmWozT3k@z;^kBTC@CG929%Vv0f3yGoc=>n zS&7rxvSE@?XB=A5*D&K@0D<$WBd`|CjJeee4v1Jut+`?f@mLNgJSg!;ngTC?AK;Jg z^uiUm%;LcMMGX#xoUpMmJCKOVVKX2Ub#^|9kdjc@7LUVnYA*KA!tMB3|Iz?lc|*=5 zY8QpvHI2mh;sg%+|GY$zBG%XV7@(0G&MzuM3_zrgpO3s%y`szZ|tjJ+Sb-Nn%cFFffV>kQdy@~z*idn0B(fF4NK|Ddsk2I_% zjVAE3Zkdv}j-R2EUl|YehU1D}yuGE(n_p`Sf0+`s_s4zV1!QNzWY3RGo6S8W5MgISDrPatQS3dgZE5EaGz6uFEYJd5Lt zfJT{}3<3S`bV;7i8(!_^iBU_Byf@Y=yvPpg22f0t%v__Q!!o}Y52Ib0P9yN6z0{Ra z-rQLw@G2`Uw|0=+L!T-Yv{focK*)1qEG?Hal$}g*T z4`C#++I_;R`of};L>%||_aI7@Jwh)Jgo5Bp1w=6BoNop`iL_{{oEma+z!5+53h9h2 zmBp1*<0-c#84YXmxrr4j3`k_(=(0kT2(vm&ldV0m8~L1p4Jbx^Nc)i&uBo9*Jpjv` z6;X*`VXbDJD39wmIz=1cDU#T+QL3nxD!eskd$!T0V9k)Yve)u(`{cn@{nf#@$Q$lI zN%&zs_wWW^6FTmF7*yJc#Za;WyM`$_Ee7aOpU^F~T`eCZE9kl*E401-IA#`wV7U9% z{`kgN>S*dVa>YA)Vv;2y-DsDsmd3P9oNc^mx#FZqW?u7hvxu5i8Ss+KS@`48F}}w; zf)39_wCD|Xnc(c|j)wu;jw>bQ~qb%26?*ffaSF)@}>Fv{yp(o)^Gm?jI!_;jSEbuc@EkIP<> z$P|;NY7z*xN|9jBy20K|{$}C?7u5ycQ8xQh@(D2Y!!Y=nNub$rw#KcsM`Aq0Oy|X= z`vJY5o_}sL3}VpOShD?dZx8n9L^!)c#&E%5)*oF@8$}^%W6E;**9cGNByXT8TIu5! znUI+_Q2J53z*YDRIgT{_{?&@G8YJc4tNQ1%PmQXPbCY3GHQ#jV4uycmV8|kyWvvdg5n`t zdOC$imWeWIz@>7$Wy?Vv1MU;6raL`-CtM;2CPi*pPM+to9Nv%msZRP0m^o)Z8u9a5 z-LCQJ(r2OO`jl=n$e|{}+Szmc4M;D;KlEI34c(@fGD^%)Cnv=IUs+`g`Q&fFUr%wD0`k6fe@l;WNI51djG-H3P54I|O0uORDOmF0fi=0WJr`|n zZI2dtR*oqzcfS=R&UZ{nXD1igU^XkHGjr-SfI}4cp;3Op)c#`N-W_*(b(W@KF^V_+ zb{OF?!mG`<4Yx{fM*245*^+)%cN1La2(#9lq48ZhkEt`6s*Pa^F4SJ?MJ+k77r3%u z-;C%rA`anSn05T{L7O+90#ku(jcHm`t=%1B!fGC&H$^ zq7ZSe=8vf6`iRal;_}D#G6~`gL{k8W(6jcO%W*b)$D33Afal3~4SG;lqR?__^eb*q zvWG9YRp!RxT61NUN8~Fkk@mtn+I{~Fa-FV~=op>Bl3=|-J%O<+z$8HKt%3ox6*W}e z-v@!fAR{WKLQ}n?uFMz&!3Z@aoBpV)f>EuS%0V&;%vYWfn-bUVkjwJT@Sn@sCEM~lQ8=bcaE{&cwuf+5eS-L&{M~F zQb$Iwg67L-4BmTPmWmSV@QsU%>RRTcE+y1Hsbd#imMB7CB2QV$C-#G~0pAuFneuwsW`N^v>En z@PvncrNf*}jxLy6)x)J>JOrxEQ1NYqwa;HxtYvuyP{H-aoFo{ygi%qb_@o{f`oO5_ zDysGQRbFVZ;Y(UZJW&;E#C*_f;&9)Vq&4g2E}?9bi!>xb9N|kskMS(chNw=G(9Rn5 zp~E4KJ*!-4ul^4?RW|_^U-V$*dbr zniz!_NNCiXw9k1?a1+;heH#d5!V*d1HqFx44%v0{1||&Bs;!w?wRd-`pN{%47rt?} zV`Pl@kok)(gjGtbnOEhbm5tk&OVaYR?&7ocOXop`(Jjj1?MqM{7fP{njfR*!-_6Cr zu42!_n$*aS^q-2VgDl@yP^x9XC2Ss-JnaX$0FPqxVS;u#}>EjFe4yP`A65ppd z1ErGCOb1(Q3ATumT`H~iyc|xuQ&dZ{&SE~4;sczrS(pG}>`?n37UGQtyt;SQp5rUp z=Y9N^jtAhXR70Ucfv^SXLWe6;wI91AJSNGbPdXRe)$y>Ki#g(j7D*DhYY`f=iZ(p# z&F0rj)Rq!RY0&=1K=g6K0$XCau%NOLrPa$shBr2jLAgnWwM+GLQBZk*tcR!5%P1Jh zp(}3APW1SiT3aMx^ejT@)K|`IyT4AoMVlJ1hr<^3hP)Bz(=7I)&w95aVp$v}rPZ~& ziJeRJ0)hgT9Bmc_>5TfK=`tI`X(gX%12+x1pf_AJ2&9F(J1Yq^ZFZ~GoJl`H@E86K zNF7aG2z$Ca+yRP4n=7||pa*4RE^XL!$`{X!#@JL`ZST?W)v7-njC=dNDs#yVl;<`< zD*V#<6lD(wUioWzyi2)<>n+~*?%NixzCMaNT#B`TVh({KWQ!wAp7!eHz+1e}QCIME zrt?Ju2rDObbsA~jxNhF)f~9>D5m0?xrj{j7=t$Fl>~rZUd%0kquq1X}cxc-2$f+4I zSKg1Y+3K0Wc(MF=ZO3l4{ByZ$$d^0OWfFAwp#*YR)JvBum$8vgWYt4_%Lp+ zpMTuUR=l}70iP{=pdIi7|9o~}*9~W;)wMqGg4qEHp^fiZx&= zBf`VGGoY-?KI~y^g6#k)x8@m!3;#2=!D-vo1(2$z+!kXbG*4RlP*vUkvNu4S7y7M*$!Zfh-e}mqvk~ z7TV3FP8Pedk@IG)DgVo>&Z>dctpvrY!pv_FCN^V}$hQ7M5YBO5u za3|t)_4F=Jgb7g(wS2Wx6^1e|mrJ7Sa^_2Gx)cKw(rTZW&6c~Lgv^?Nt?m%MwO$jL z%2A#qIF1K_z|yx?v_{g6$MiqYxE5nLX7i4 zwPZUY&~JUq1!4D!$jvs3U6}Ot%KQ!06-1O_v$+!5LhzZW8duvc_@n-eigB{%n|u?; zke(}lS$0BC@-(ID-Ai-|z38+G3fvS%9zfA=5tM;e0^J82%>2~pS4>q5*Z16!ztP&N zM3A;^F2z51QGfZ_LvIJqO_*>4-&```#EcL*4?Oy9(Aht+;40)#>M}0!%*w}>M?6Zu z0U1pL8&fT7?P2UfoDQL*1Hxj;@L}^E9i0sy%GNgrWa=XX?&D>Kb?c*E`BcJkDFawH zm)_b!G&)%nPUuk=&9(aD)8qqP-pOu(eLb@O{`kOez(cgaYU||_=TC1wFdToxmFcs2 z6xH3+Gi3OXPUu?aX`m+fBS^GnW~pKg-Acni0*6Gu+a=nIYM;DQDwnj3B+506Vs@Xu zTUXbYaB&YbJXVWSaeHzkj@%eN1-SU6`$XE2+BJH_Bi<^T#!oL!G9G3!_nW$7C+mfY zyTk;CIGZ`JGWLedLVbQ=sTYJc_@MVmq`Ggg7E}jC(t5WBwQ=_?5p*gomuy8Cl3A1k zv&#dWFGZYl{^F4pZfrSz>tSh>5NnAYdtJlbq{9Ol4ect^7>a2JG@iv>4(+Wwd!#;( zCTuVV#!j5a)k}=thrP2GY3sBOJby4J22cs}X z@k3pn+ob2lyx@G{#^SaasL6ll#=wEK$KLfiP{Rkx;a2IE+#|C)j)xkqJtWOHqemR=+x)s3Zxy57bH&HhHf`jvxdihzdzH?$;86277naJ1 zKz^?TL?Gf)46;?--XRJ)yX?H3G*DXp;KXU+FMN}e34eP25>7JjI(|0lXlJ*ivoqG< zs2{$PzQ!(wd(pygRfcA@xAr}^q4mvRb=WAV3al~8%bFJ3n~%tuKoOd|L}uW#bZ0{8 z=MP&~`ZaD*32T*OA5`tg1lGMV>a-k>nz@^pl%|@Z*;^ObT$AD}1L;A0KmHA;S`IK< z!ThhSobTEMQtG^9dTQexT8o}99ok2A7UU6wlpBFQ3EG#sRymgW>{5^u3htp)aGe(x zMd3qDxPi{z@bIqRgr|Uof#cR8y2gfEJYidN-=!f#!Pv*oH*-6J_1c?3$J_P}9OzZ` zcHRV3Azn444zB}x4sm)+D`0w2C6;N9uuPq10oaJL-4nBEW2qEUf@1!B4ajG!q(c8a zWJ_$L+2MgA6k4SL4M6UTbp9G$HlD%GEkn9UWra115e(#BdyA@os`ZRY5JUrk$+s@2 zWcC$nqOcm`QqElKRN{h!^eRe5d=ejQ_y?tVBH;y?bF$gI`~mQ#2C5K~7`Sn-{^+lBMW;Hx%97jjrw=|&&sAvpae1$mf zH7xAw{_%CY)bc>^O2J~*U9TFEVQ61f87sAA!^nwxU#1wDBxYase7syAR1PZXcE#rY zj14eI_N%?yWJSbyZgN=Vce;7)xIt_pz-KzKN|W^hT?qS*;cz4F&{|qYTFfWG z1qn9G67juDn8KqLW*H5A!qCzy^0H#|1^s4nqsyw>x+_sh-2kD?+ z!e<%X@3S-$0&9YL(cP+}E=8ajMe-^KP_Q!sIZ~lprd^f$4599k{t><=RM&Yk{o}xo z&Z+e1#s>=bBhlro`%X=5qjt|jscuQ>c2jyc*!9A|pyKv9$!5q*i-h5RKVv4ebQV?) zwj!TFcb7>$eQqyNq@^ZvswzsYyyt~)QfkX(w@BX=Hhw|=aeRJIa;*oL=&^ayGai#Y z|ImVE5^C!aF-a*kB&|_PfzI~!sF6|;)NTw;uSDuaaHFwhL+g|m3g3gO;zMe(Pc@2v zv48e#-n}&?Ntwy>qy{j>^J~TqBG4yoikNSKFuJtLpOWig#n&$b!UlWuW4n+w8!WaJH+6YRHi=_qCDTw28AuWaV;< z2fH9qI3&;gOF3iV9AnwC(GH}S_sp)w4FPQ0Uh%dLEPF&+`DN~64@!!3WpMN56BtmUrY>{Yf?#58KORvR+M*#P^dU|8n7RfDZ%Dd0 zncQcZy6xx1;ZH70P9m}hXKcLkK4F%G_r?s0D2$XB@1D)e2;zvv^)^vWw_+`TRx^-> zWqZFR$B09;{kt<*l++S8y;d9ql_Oo>*hbHtZk<+o0h_zpm&BqzSoG762h5n0samjn zPQagDR%I?CW-f`5b&OrkUOtrRmuvjxLvcyo$vyJt%Q@e!e+eDgjtDqe`K;w9l@OXB zEYsG~xUMTKW9tcVAQ$$H)N8^9syyt(=VbUw_VV%zcw)K+S|9r+opJ9bQA_1Bb?9|3 zN7m)4#|8s_oTHi7tzU15sJY>>>o+f~88qJ_-$2FNcRdt~NmdSJOz#HLqkyvXF)&8d z2;_ZssV)lAJ#Yz4OErmi<`T3kY0z1J*9Zm!7Y#tP(_&xiy)v!wn1T;ER#u#t~h?6${a6Q{srld zy_5N^*4~ozJ5pnB!U-+*!r=@}*i^$?-w5{C>3cuY9zXZ*zkr_WiH_>s(_fk|Wo6hg z6KHc~t?3w5L2pd3W((NBTl~ttsS9Q?BCkay0_RfieEmgo7U8?1dj#1xeE&^{Ky~lg z+-C;%Q@*cmoHvvV`@-`vRdUekS!>?)=li!dB|Q>)Iu8s8#mBwpe#De4h0TLN7(88H z5?@yqBidWi>5!-B?UIS$gz@3rLp`wqj=AWCX+D{=a5Dpb#z{RpX9z*YHTjS*oibCCR*;~T31Y15o zjp1#HG6|1q1*cy8-MZ4~Zx{5xuTu>^4ivAsCf z6?-SFa-%a5^mD;mUyQgxMWQa7R9#URqS0Ike7r4@De@up>7=aU ztu4>{t<~q70t}S8428Gszn}CZdZA4(Azm~YmM&#=;7lRJd7xgyR2UT@a~l<%hK>`f z1uiV&7BWdJG@+9?KM}>^E`Qp26en#{#zrdZLgUDBrS^e_H_iJOTAHpL1A$Sd!57aQ zrIBKsB9sGL9^yBBH4ui&PKb^Op2E&%+0cyKd)A~D;3DLy5WX?EdpM!!Zr4C~7s#F7 z?Nw+a1Ip5Uw$PddZfZQ4`AcE(DF5W}OQ3VQ6#BIrU$gu)G^#Z%RkexjL&@yuS_qjL zwU`d4SXxnoliTah+7#A>cHd-|7IDKxs1*TcBaFsauDJ3(k3W#=DvNeYzdDb$yl7m%ek(25y}51HuvE7s2}UMzY!FVVAwFOe z0!5A)2)wC+if+jS7j8+0K-a#ZP3Y6H*vqn(+>s$25ZD(d{?JNBk zofCeXK)f$uWAu8S{({7S0=1*KOJi~>OPh&R%9&+JO@%b?ZF0|LkxcW}J{^$+@MXc? zy-oV1CGj&nvTj9c(d6E-NMj$y!#fc+-RzQ?Q6sM^rx_Ij0jAdWDLvHLUhf=I4>V1& zBcuU6h?3=O>8`|f=dg+n2*0%&S{ecaDf&9tVekBr(2&lP8gPeGSpA%mL9ifV+B4HMk5-- z;*M9e&$)Cx$W2B_cgGk%vyX^hxG@am-u&2^o%W_Qa@mquv2}I+ZBpvIa!|DageY7{<7Fu^PIdPhS|^RTf& zpMQ1`&xhHT%qwCwf-1HMW0c}Ju-vuQVi5(1o+DN}&>iQ)O3f>6u;V*rggR=1 zGMqLT+|{gX?JTI+W=C3g9*I(jOtV3%u||8-63}RpN}(us=zODCA-qRqw2xME!G<5~`c$SG+>S(sI z_T?ucG@(7zHCl{+Xqw7ISdz+Gm^>7u0DD)f)R7~99?drPXIdPf4x51Q>p8v?X-$J0 zZPsg;^o^fL13|t6<>GaKt)8dXA|6wf%}u4Q`O(1jmbSGeB>@R$k8 zU1&)m%hY_d2pxW#D4Auc6>yz!`NYLBBY=tA-@8Qu0@eqg#l*B9sZrrN?|J95@R@Iy zj>)w7@FWs*UYYrrKCQ6&UiBOB$6KYTEDq5Qjdx4M_G{;|bFT|~e9n0%e>2x}Q@-r( z6-OUe)~qCloRTK|1bG=xsH!+b|aueDf&Mn153hU`04@ zSBAj8arlvbZeq)YeQ$+R9(0YTGjovBpbA^d$@vXV$elUgdpeDhG<{%Z3HZVd^^*Aw z_zf_5m2T&w{Tnb@9X|D46>@2qeCzc#@Suzx3^*b1G2QOSF%IC>B!gO|le`0LyH9?K zsUDM1A-NlAC5*zom!K)0qSJu=S;or67cvycr3v3h3W2_LhMaXE5L$SDB53p59q474 zRK6CR55l5=UzkMC3&zL>c9EYrF`tAR(tf0kJ26HMzrFUfq(mC9sB;d(QZ&#D7P3l4 z;kz0!M^EK#9Xi@q8-_2g-QM)}2l;ohJr&XR&U8$+1suz+XE3b!V-sO3+@_7GjX9!1^q>*icY?&S&Eczy4(KLfkyHfXo$C5^*r0Vr9D*was@ z60C_ldV{?c3ZYG~_gy*8PO;)jNx>R_lqPYc(@FGYw5oS^oqK%orHa`G19wl@V5@hb zn&P^>)cR(Uq@9d-0=ak8?g5S5d)YWN>K0M2c%9&oudZ2B;7(U?P**mjHJdApG;qN+ zuDeu_4K zbC2yeMP}drqnCthN98B%gkc=mX9qXiwJ^81>gu4*sVv&cP{!CKS?bJAa)s3t&6qjE z-lj|+pN0Ey#yhVbE+j9)AINqOtUPaGU-sx8aymr$4CpeM!p0#j?7{cSzTwUKsprM7 zyW?)#YrL29)V(s-#4$+d=pAjZ6vJ=@V1`8Vl2Shw`}{xf>5Ps1uPxv*H+u}9!NQ{p ziR;%wNrBGb34g@?Qhm100v_#WSfcYu%sRtBkvXBzw2C4G_}}#(b=6ynjv%>dNi?hyU_~t{-a>(3-I0Mp0 z?#p1l)KTBoJ2&ruV+{7k{N+NAYnh3h9UZ1auC&=)kzH2Rg)pf| z#2T)znpyj#xsjxABfY*`)ceHS+m3e6-kLguu}RIE_QIVsEU(NK3{;Tb1YQmxd95UG z9iR5?D5vjJh@Wt?Xp`H^y$xVlnVdUJZ$=jPnMO#O_}7XSgW{8S#PNJzD5pS`{7zQX z0UYc`ln$%Yt>u`k>&n-q+c;X=R<%wds8(LY#Q1gCq7`Z5Pp_`gvia8qNrnrQD)WZ= zc(1#2hlkl*kZeG`9l;s}7Si<*eqnr&66s@cng8|ha(3tm zzF%VpI+2-oR3qF9>zOG_!MNW1DD)}PDp7^WNxGLZhYd@j zvNL61fb<@V_F7GaOKjinuS2c=T!-7ppyVq?h5OgAAi6uO4x4?^EYUBR9fi?d-8DX^ zsZH%zi!OCr6CKC7&u0pRoP^S(U0fpC7ac}7xahezm+*@pWfr`E@fxFs$F=MH^<0X! z(O8-Q`#QBf^$sGH=vo>0ayB)^}Oz)d_Z$rDii79ff^|b0=K#fR-nK>z&HPmkRZe z?YMhob=Qgb@&_|m_^?-mc4v{cs>_6j&6HDnp-9W2gXhtyfUw}`P8D8r z**+>@M{qF04#n0i&qAe(;M@_LH3VihdyOj~kRk;gs&vSoAiljIhpU-!-z`+?PXXi& zs*um;4yuw~7Hfc}&kC8oH`=O1s$jcZ++&Fmnl5CZxO!bv`GQs3 zmDZOl2-X$Wi)by32r;iJ=y-lwX{Mq_bfqH;J+O}2^);c5>a(#t$hWgfgofy5%{nW> zeNWLkCqleNNj;Yu37@4eQ7)Xd^;LJK#b^(>H68gbW+N1`5Qtt&vYHK6McCEcz?jRx zLt%Bz?u{6D(Qwup*&qpFEnCQ4mR)ag-`Y!S+j0fp7vLlvve`89{rYcR=M0Im2(IyPNIi=NE;8Nmhl5lx`Uasscxpj?|*(sE2n@ z#hhkYGK@>_;Z=m=!han!J6XP!Ot;B_MtxebUJf7liH7Bpy1O|ukW@uy|}UeazFG(~xA3o~m)viLhrTDEz3{pFJnjT4=Fk*&{mSxeGcE8%_wu zq9$9H0MvpZzz~PPy=Tt4B3UFng;elj*7?vSomA**D^G$0rQuG*?G+udVCz z{@O)plJ9LK_aEr~sGH)RTv&G9R7V_GY`n)nm)_M+&~DZe;jeJc5QL0^oQAmk; zBbEhO6RrF2r2FLfgDy_D&+M;nAU6-O9@0 zW1$MIOI2;82F57cJ-n^C+ue;#Y0h(jCDbLmTYY#?V*cZs3} zW2~_(t7Kl_y}U&bE4Yw5wEV3lDKmw3iOgAS>hxHhpURRuAKWKN13xU`oobZ*al5+X z)0Yg+a&#!c4dP+Jj5ECR39Z?1;Gm_9a<(dEJ7Crh(7w$6hUU_Spl)!mS02&l9qJ0^ zx<3NPkQdH?;6UZ2J?=cBn6U`=a=KGi9>yHy{l4iM@Q-!C?D^y+@C&khBbcl;cozkW)Oq<;JHkl z#JX34WmwroZO#3g7(o%ViMxACx=2#ytOT1AXMibBL24>^G>Bg>(FV&9EZ_yW%co{t zfNrF~iXdeCaNG->(A!+^4lD zs(6>h@hj|A-sUVrrfD16RhN`8|MCy<;fh?&utLd2W5z2tcn7asYjG6ONrSa^FLy%c zt*f!fGq(zUz--|u=ZhTuN?#clpnVEc39ZGI+g5!?B&rj2rBE@|uN9s@7$IT(ZE!=z z`e!~?daO5Tb(^0Hq~%hHPMcyX7)TOZ(>(Q?`|s#*Crn=@yCa5M#5Gpav_<6_&^C1j+#(&P0Pm9Mi$8_ zk(oVcLX^J;E<&Ighc6&vSQ8+-r6>kWqB+96gfYn7SZr>SXIRR_j>8M*AqT!7aU4{x z=v{W|)mWDZgKhe;2>)!`0YT>cbd4w%yTvWR)t(-d;&I<3qjRvdeG26n%-&Q8>U5jg z>(s8)qF`d6|4dNZV78&Dh*WP+-C_=~{Y;M*wnG)rE6}>lFW%V6l zCVu*e%$tTezmAH7I(7%8VbB?n{aoe^@ZH(05+h7n9u<<=RmypNft^v~!ZFa}D;3;^0~;BbRbBfs-qW zD<0ZQY3jSw*BYUz5Kb4cewWUeUwBmn*1J1YnbtyGP0(x>uA!}t zIYewD8jNmu&_{k6Ah%FI(eQ-g zi}mpgBmW=ly;oF|+q*YPkq#C*0TGcJKq(1Qm0m(kNCF8>Itids6%-d;2}C+VC_+dA zskDHKm8w)JNswT#5M(WtvJ{uDFaQ0GGseC+#^59g`85FHahufxZepuE#YJ!D+dDeIaxFfJYg|~p?zMxG~gMr+MQw<`&dio zTx_O}ZGEx9m;Ad_iNB=sKK5L-<7vE*fBWV_icfS}K)C7_Y$K6wyJ0;^!O;g=ll%bdW9+=*N>bE_1PdqjF zkIH7wu{YXZd{dNO*GDj{6{PqQ`QBG@R1PYWF(~I2Z`4#uiN0lEG?gUcKbUjZu6|*M zq}*;dM9cgT?+CO=@k1nzgIlMyf}jae*B`u+8}J2)!(LfEd}B=6c{xEJdO}s^qMSLQ zpSO&;35!@)kHx?VcTI5eFc!4pXPp#vW-739+zN(Mv2)1zHu{ot7uM`)pQBEoECs4Dq`FMZ!s;wus;w>NSV*>BNTTAc)|9socD zn|g-OI%_}%!+mR|4AlOh1s6@kObghGgD1hqB(=9Euf2q&-W_$mfhBsUuYN6m}0oK#^#>BYt}JWB#uojZxqXlJBo!G z&@GOo5TAwy+q^?oQU~~D$pHxI-?+N!%J-hy-Ouz+@miBXOJ5!~<-dJ3b4R*l$v*a) zbY98m_7^9oFrCv5fT3&BUftI}+@@(yQfa(C1&_xh zl`jnJu!_;B2A$6vFlwYc0{%2 z#4k>p`Euk3b^D=hbC}~+N+9LNtA?kj#%_xjz`@_X7k>I{bnib|bLOhb|IV68bs1e< z*q}KFZrm9vVJKvuw9}h|9@mr@hJLq`@S93{woe`0&J}XepI#o&ntGjcp$EB%|m718p3z<>Jxj0F3{lHfzQaWj=aY!k3peOmTSa!8PCKknWWJYzIN^?B7@a ztmWj=t*B(gdFJuEs!|O^-T&)z;?N->qjt>ABRWLxy7Ymep%2|da{Y*me`X!AJeZ&f zSVT>pTkY`sxa#&!K4D=8oe~T_>^A)1o{;iO$cL6!v0|LoYI^?Z$Kiw-uHfB zzCXyW(X<^PpTC&-Ofz->_%6PMuzP`Nmq*zgP{oG?!?+0j6!Y3O!dn{7y_WsEQ8Pp! zPq9ao_v8pfcjh%p0c(grMt`85#hFo;Qdi(DM{Q-v%H`Sp-QgXV!CcRvvWP>#F8VA5 zdz`4I39=l(=5#tn1v&exE;7KuG!)e{H1no6H`Gsp29m>D<2u4=rGD}F6AhG>yoQgB zQ=fP^HP2x5>jAQUII!*2Yzt=pD|;xi<@8GM$kywhR2&Lg>ru8Oeq&I`GK%l%q7j>H z3HKN?$qUw_nQ=ojZZZr}KBu3l(|U%A2RTVp!J3@XAtnxO2n zJDnAK3D+t*-cEV?DAy?go7u})OL*-e?i3a)>}hb9-1+(S4tnJR$FP^OH)n6$GI$I+ z5EDN5Ol8HncFoct)EHG5GEndoN#-~zAt<~kGBSEGL%^iCF*87`yK+D;Q@CN76!&~vMrCc7LmzKFQ{f9U2Uy<$5#hS+?yTjq1 zw<+HElS_VzRi93|cJJ<7{jCan{^_^;g@D6SuhHKg*tuP&#u^Q}_!}sg@O^?g$Le?Z6k$&*u$V zS;dENAC1I|-k2}_g1@7E-X`|cscY3ru|NBZ+Ps6RCzfpfF+X$T_X(rcyXp@P9`iqK zX4!RO(lRk9&EF)Wero(>fK&;#t6qt4{-VL4J!mY^A0aNEU~+Fk{F08BZf1DvMMLq~ z&3z|-?&myfQxfl&ul#YSW;_d(boK9?1RqNCmk*yN@3E$s9arsj`?E5i;^rLwRxtL8 zzWB0h8{T|!)E-v4(2flG;*kuS2a6CYw~(k$`ZiL1AbY?X>GYF zs2vjS?X*~7a*GLPG!rK7uy5B~oia%zy_r1r2l3S(+Didh9jmR~yLkH0R5UcD5_>6O zi%^sr)C;qui&YFNv6T-J>G6(wU?LA`9x~l|#K+z3W9m!G0(e{Mr=fFg zb|-Y!%EE{vlMKr2`!94)OybA41N{ZouW^R*Z;Qvl#>PwH2|DXf`eyp%>rJ#C1MoZ* zIaH;)cImS9kJi8&A{kD32amubts;Yz<5|32=9hZplwPvn{tAq`ZemwveS=%C5bO$R zl2@!|(Q|INTZ@4d{5S70##*rhMa7v(ox z*n}>5%%*EEH)UyRG8F33gU8Ss^DZFYA8pTsGAB#ejN zkjU6ov?gnX63e#p?ZC3W(>ntX7LX*Mm#?Q+$V8h97LKt`B9>BEHSc_O#6$l%aO%Ls zNg=uk8RyGWoHdR{99AX5DXqMXtWsA03aGTyGXz`E)rPHp7f#>_Z-xc32?J@d9@MXv z>l-a_`()CJRcZ-ouD>UGzvRYLri$T@$yr2BlmRSg;-$hizpCX*pLuLgQmp~ zIeOSwp$KONRnx4|1A&@^_G)s<1%Yu#!d#}KndS%eW6hW+0NI>WP`w#>PHpW2zL6Fn zbtCVO5#{G3d&gvVw!ys0AM8uVuMetXcXIlnvN^UPkIk!OzikEa;<*YP#-ZMRL2iw} ztV{aco@gwCljN7KUIwJeNqA3?Uhs{-e1b?)FPhwwXRBd;(XyVb1SJjDMFj#9`u)_x zcPz)nQ!GOSYwMQx4H7I>2%AS@mBZPW(75it8}qkNZLFB1mRq(MzeFzEYw>oHvY#H$ z?t*wWEoeC5wH_baXPh+#)?k0Qvgk-(YtpTe1zurSh5MJl0FkbZ8$foWoTxiCpWds< z&aNJ!uvh&b$HG+VwpM5xx*d>UE?22j)`kd6fxh^@{?UnuxKZ%)QDLyk8;{FJ?z(Rp zhjIQ+neuHccA*}N&cksJ(`6k|oN{j5gu4oHV*@0^Xe-XOWvUqD99vgHT~yom-jhi+ z4-QsHXtN>KE)xF$^6vHusnA6+u`2kY-EmfU9!`5n+AtUlA^3_g|5Z z`T;UWXn$X5%Nb_xK6xNAWIULg1i!x2Yq%gFv+w+1ZI$%MD$=`nDN&Z*gjngI=wdU$!VK6k{0~GWOsx-kl@8R zT?_*rh#KOOL9`lNt)SE<*esglRt?snl5@14hsRr0nzf@5fxa&iL1?q$OeKEnV|bM;B38NC zKW!-IfnyuTtnZ(FJ|O!fW-YcO1~_RqDDX)ENwR4pjVBf9Y(7Y(o-zj|nGM#NC-x}M z8x4d2ITq&yv2)2rMn1n^L_C1}EA`zE-a?9bTJ`a7kDyx)l zh=Bx00^3HSc#bhprAp%kfD}Zn(!^AAPyAH=qH2lj29GoL z#Y=we1q{Zj7}Gxe9rX{u@3iFRg@v3`!<3__0ktr8oa3o3!Q?*36WA@Iuch4LY}|qb zXtGWf&CU&{jY8^ZPi-{tWXVx5r7OC3L(X(Zv%SnVDY#JKyQRRgZMR_++X0lrSl + )) : null}
- {(product.name?.toLowerCase().includes('pro') - ? PRICING_CARDS[1]?.freatures - : PRICING_CARDS[0]?.freatures || [ - 'Unlimited blocks for teams', - 'Unlimited file uploads', - '1 year page history', - 'Invite 10 guests', - ] - ).map((feature, idx) => ( + {/* Use the same features as home page Pro Plan */} + {PRICING_CARDS[1]?.freatures.map((feature, idx) => (
{ }; try { - // insert product to DB using PostgREST API - await postgrestPost('products', productData); + // Try to insert first, if it fails due to duplicate, try to update + try { + await postgrestPost('products', productData); + } catch (error: any) { + // If it's a duplicate key error, try to update instead + if (error.message?.includes('duplicate') || error.message?.includes('already exists')) { + await postgrestPut('products', productData, { id: `eq.${product.id}` }); + } else { + throw error; + } + } } catch (error: Error | any) { - throw new Error(error.message); + console.error(`Error upserting product ${product.id}:`, error); + throw new Error(`Failed to upsert product: ${error.message}`); } }; @@ -50,9 +60,20 @@ export const upsertPriceRecord = async (price: Stripe.Price) => { }; try { - await postgrestPost('prices', priceData); + // Try to insert first, if it fails due to duplicate, try to update + try { + await postgrestPost('prices', priceData); + } catch (error: any) { + // If it's a duplicate key error, try to update instead + if (error.message?.includes('duplicate') || error.message?.includes('already exists')) { + await postgrestPut('prices', priceData, { id: `eq.${price.id}` }); + } else { + throw error; + } + } } catch (error: Error | any) { - throw new Error(error.message); + console.error(`Error upserting price ${price.id}:`, error); + throw new Error(`Failed to upsert price: ${error.message}`); } }; diff --git a/src/lib/supabase/queries.ts b/src/lib/supabase/queries.ts index a8b9caf..9564ee9 100644 --- a/src/lib/supabase/queries.ts +++ b/src/lib/supabase/queries.ts @@ -1,6 +1,7 @@ 'use server'; import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete } from '@/utils/client'; import { Subscription, User, workspace, File, Folder } from './supabase.types'; +import logger from '@/utils/logger'; /** * Retrieves the subscription status of a user. @@ -23,7 +24,7 @@ export const getUserSubscriptionStatus = async (userId: string) => { }; } } catch (error) { - console.error('getUserSubscriptionStatus error:', error); + logger.error('getUserSubscriptionStatus error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: null, @@ -43,7 +44,7 @@ export const createWorkspace = async (workspace: workspace) => { const result = await postgrestPost('workspaces', workspace); return { data: result, error: null }; } catch (error) { - console.error('createWorkspace error:', error); + logger.error('createWorkspace error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown database error'; return { data: null, error: errorMessage }; } @@ -65,7 +66,7 @@ export const getFiles = async (folderId: string) => { }); return { data: results as File[], error: null }; } catch (error) { - console.error('getFiles error:', error); + logger.error('getFiles error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: null, error: errorMessage }; } @@ -92,7 +93,7 @@ export const getFolders = async (workspaceId: string) => { }); return { data: results as Folder[], error: null }; } catch (error) { - console.error('getFolders error:', error); + logger.error('getFolders error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: null, error: errorMessage }; } @@ -119,7 +120,7 @@ export const getPrivateWorkspaces = async (userId: string) => { // In a real implementation, we'd need a more complex query return { data: results as workspace[], error: null }; } catch (error) { - console.error('getPrivateWorkspaces error:', error); + logger.error('getPrivateWorkspaces error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: null, error: errorMessage }; } @@ -157,7 +158,7 @@ export const getCollaboratingWorkspaces = async (userId: string) => { return { data: workspaceResults as workspace[], error: null }; } catch (error) { - console.error('getCollaboratingWorkspaces error:', error); + logger.error('getCollaboratingWorkspaces error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: null, error: errorMessage }; } @@ -183,7 +184,7 @@ export const getSharedWorkspaces = async (userId: string) => { // This is a simplified approach - in reality you'd need to check collaborators table return { data: results as workspace[], error: null }; } catch (error) { - console.error('getSharedWorkspaces error:', error); + logger.error('getSharedWorkspaces error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: null, error: errorMessage }; } @@ -205,7 +206,7 @@ export const getUsersFromSearch = async (query: string) => { }); return { data: results as User[], error: null }; } catch (error) { - console.error('getUsersFromSearch error:', error); + logger.error('getUsersFromSearch error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: null, error: errorMessage }; } @@ -221,21 +222,21 @@ export const getWorkspaceDetails = async (workspaceId: string) => { if (!workspaceId) return { data: [], error: 'Workspace ID is required' }; try { - // console.log('πŸ” getWorkspaceDetails: Fetching workspace with ID:', workspaceId); + // logger.log('πŸ” getWorkspaceDetails: Fetching workspace with ID:', workspaceId); // Use client-side Supabase client for client components const results = await postgrestGet('workspaces', { id: `eq.${workspaceId}` }); - // console.log('πŸ” getWorkspaceDetails: PostgREST results:', results); + // logger.log('πŸ” getWorkspaceDetails: PostgREST results:', results); if (results && results.length > 0) { - // console.log('βœ… getWorkspaceDetails: Found workspace:', results[0]); + // logger.log('βœ… getWorkspaceDetails: Found workspace:', results[0]); return { data: results as workspace[], error: null }; } - console.log('❌ getWorkspaceDetails: No workspace found with ID:', workspaceId); + logger.error('❌ getWorkspaceDetails: No workspace found with ID:', workspaceId); return { data: [], error: 'Workspace not found' }; } catch (error) { - console.error('❌ getWorkspaceDetails error:', error); + logger.error('❌ getWorkspaceDetails error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: [], error: errorMessage }; } @@ -251,19 +252,19 @@ export const getFileDetails = async (fileId: string) => { if (!fileId) return { data: [], error: 'File ID is required' }; try { - console.log('πŸ” getFileDetails: Fetching file with ID:', fileId); + logger.info('πŸ” getFileDetails: Fetching file with ID:', fileId); const results = await postgrestGet('files', { id: `eq.${fileId}` }); - console.log('πŸ” getFileDetails: PostgREST results:', results); + logger.info('πŸ” getFileDetails: PostgREST results:', results); if (results && results.length > 0) { - console.log('βœ… getFileDetails: Found file:', results[0]); + logger.info('βœ… getFileDetails: Found file:', results[0]); return { data: results as File[], error: null }; } - console.log('❌ getFileDetails: No file found with ID:', fileId); + logger.info('❌ getFileDetails: No file found with ID:', fileId); return { data: [], error: 'File not found' }; } catch (error) { - console.error('❌ getFileDetails error:', error); + logger.error('❌ getFileDetails error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: [], error: errorMessage }; } @@ -279,19 +280,19 @@ export const getFolderDetails = async (folderId: string) => { if (!folderId) return { data: [], error: 'Folder ID is required' }; try { - console.log('πŸ” getFolderDetails: Fetching folder with ID:', folderId); + logger.info('πŸ” getFolderDetails: Fetching folder with ID:', folderId); const results = await postgrestGet('folders', { id: `eq.${folderId}` }); - console.log('πŸ” getFolderDetails: PostgREST results:', results); + logger.info('πŸ” getFolderDetails: PostgREST results:', results); if (results && results.length > 0) { - console.log('βœ… getFolderDetails: Found folder:', results[0]); + // logger.info('βœ… getFolderDetails: Found folder:', results[0]); return { data: results as Folder[], error: null }; } - console.log('❌ getFolderDetails: No folder found with ID:', folderId); + logger.info('❌ getFolderDetails: No folder found with ID:', folderId); return { data: [], error: 'Folder not found' }; } catch (error) { - console.error('❌ getFolderDetails error:', error); + logger.error('❌ getFolderDetails error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: [], error: errorMessage }; } @@ -308,7 +309,7 @@ export const deleteFile = async (fileId: string) => { try { await postgrestDelete('files', { id: `eq.${fileId}` }); } catch (error) { - console.error('deleteFile error:', error); + logger.error('deleteFile error:', error); throw error; } }; @@ -323,7 +324,7 @@ export const deleteFolder = async (folderId: string) => { try { await postgrestDelete('folders', { id: `eq.${folderId}` }); } catch (error) { - console.error('deleteFolder error:', error); + logger.error('deleteFolder error:', error); throw error; } }; @@ -339,7 +340,7 @@ export const deleteWorkspace = async (workspaceId: string) => { try { await postgrestDelete('workspaces', { id: `eq.${workspaceId}` }); } catch (error) { - console.error('deleteWorkspace error:', error); + logger.error('deleteWorkspace error:', error); throw error; } }; @@ -401,7 +402,7 @@ export const getCollaborators = async (workspaceId: string) => { return { data: allUsers, error: null }; } catch (error) { - console.error('getCollaborators error:', error); + logger.error('getCollaborators error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { data: null, error: errorMessage }; } @@ -423,7 +424,7 @@ export const findUser = async (userId: string) => { } return null; } catch (error) { - console.error('findUser error:', error); + logger.error('findUser error:', error); return null; } }; @@ -439,7 +440,7 @@ export const createFolder = async (folder: Folder) => { const result = await postgrestPost('folders', folder); return { data: result, error: null }; } catch (error) { - console.error('createFolder error:', error); + logger.error('createFolder error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown database error'; return { data: null, error: errorMessage }; } @@ -456,7 +457,7 @@ export const createFile = async (file: File) => { const result = await postgrestPost('files', file); return { data: result, error: null }; } catch (error) { - console.error('createFile error:', error); + logger.error('createFile error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown database error'; return { data: null, error: errorMessage }; } @@ -474,7 +475,7 @@ export const updateFolder = async (updates: Partial, folderId: string) = const result = await postgrestPut('folders', updates, { id: `eq.${folderId}` }); return { data: result, error: null }; } catch (error) { - console.error('updateFolder error:', error); + logger.error('updateFolder error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown database error'; return { data: null, error: errorMessage }; } @@ -492,7 +493,7 @@ export const updateFile = async (updates: Partial, fileId: string) => { const result = await postgrestPut('files', updates, { id: `eq.${fileId}` }); return { data: result, error: null }; } catch (error) { - console.error('updateFile error:', error); + logger.error('updateFile error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown database error'; return { data: null, error: errorMessage }; } @@ -510,7 +511,7 @@ export const updateWorkspace = async (updates: Partial, workspaceId: const result = await postgrestPut('workspaces', updates, { id: `eq.${workspaceId}` }); return { data: result, error: null }; } catch (error) { - console.error('updateWorkspace error:', error); + logger.error('updateWorkspace error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown database error'; return { data: null, error: errorMessage }; } @@ -528,7 +529,7 @@ export const updateUser = async (updates: Partial, userId: string) => { const result = await postgrestPut('users', updates, { id: `eq.${userId}` }); return { data: result, error: null }; } catch (error) { - console.error('updateUser error:', error); + logger.error('updateUser error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown database error'; return { data: null, error: errorMessage }; } @@ -549,7 +550,7 @@ export const addCollaborators = async (users: User[], workspaceId: string) => { await postgrestPost('collaborators', collaboratorData); } catch (error) { - console.error('addCollaborators error:', error); + logger.error('addCollaborators error:', error); throw error; } }; @@ -571,7 +572,7 @@ export const removeCollaborators = async (users: User[], workspaceId: string) => await Promise.all(promises); } catch (error) { - console.error('removeCollaborators error:', error); + logger.error('removeCollaborators error:', error); throw error; } }; @@ -607,7 +608,7 @@ export const getActiveProductsWithPrice = async () => { prices: prices || [], }; } catch (error) { - console.error(`Error fetching prices for product ${product.id}:`, error); + logger.error(`Error fetching prices for product ${product.id}:`, error); return { ...product, prices: [], @@ -618,7 +619,7 @@ export const getActiveProductsWithPrice = async () => { return { data: productsWithPrices, error: null }; } catch (error) { - console.error('Database error in getActiveProductsWithPrice:', error); + logger.error('Database error in getActiveProductsWithPrice:', error); const errorMessage = error instanceof Error ? error.message : 'Database connection failed'; return { data: [], error: errorMessage }; } diff --git a/src/middleware.ts b/src/middleware.ts index cdc7360..8be477f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -6,7 +6,8 @@ export async function middleware(req: NextRequest) { // Debug bypass option if (req.nextUrl.searchParams.get('debug') === 'bypass') { - console.log('Middleware: Debug bypass enabled, skipping all checks'); + // Note: We can't use logger here as it's not available in middleware + // This is a development-only feature return NextResponse.next(); } @@ -28,19 +29,13 @@ export async function middleware(req: NextRequest) { const isProtectedRoute = protectedRoutes.some((route) => pathname.startsWith(route)); const isPublicRoute = publicRoutes.includes(pathname); - console.log('Middleware: Processing route:', pathname, { - isProtectedRoute, - isPublicRoute, - userAgent: req.headers.get('user-agent')?.substring(0, 50), - }); - try { // Create middleware-safe Supabase client const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'http://127.0.0.1:54321'; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''; if (!supabaseUrl || !supabaseAnonKey) { - console.error('Middleware: Missing Supabase configuration'); + // Note: We can't use logger here as it's not available in middleware return NextResponse.next(); } @@ -60,31 +55,18 @@ export async function middleware(req: NextRequest) { // For protected routes, verify authentication if (isProtectedRoute) { - console.log('Middleware: Checking authentication for protected route:', pathname); const { data: { session }, error, } = await supabase.auth.getSession(); - console.log('Middleware: Session check result:', { - hasSession: !!session, - hasUser: !!session?.user, - userId: session?.user?.id, - userEmail: session?.user?.email, - hasAccessToken: !!session?.access_token, - expiresAt: session?.expires_at, - error: error?.message, - }); - if (error || !session?.user) { - console.log('Middleware: No valid session found, redirecting to home page'); // Redirect to home page instead of login page return NextResponse.redirect(new URL('/', req.url)); } // Additional validation: check if user ID exists and session is not expired if (!session.user.id || !session.access_token) { - console.log('Middleware: Invalid session data, redirecting to home page'); // Redirect to home page instead of login page return NextResponse.redirect(new URL('/', req.url)); } @@ -93,25 +75,17 @@ export async function middleware(req: NextRequest) { if (session.expires_at) { const now = Math.floor(Date.now() / 1000); if (now >= session.expires_at) { - console.log('Middleware: Session expired, redirecting to home page'); // Redirect to home page instead of login page return NextResponse.redirect(new URL('/', req.url)); } } - - console.log('Middleware: Valid session found for user:', session.user.email); } // For public auth routes, redirect authenticated users to dashboard if (isPublicRoute && pathname !== '/') { - console.log('Middleware: Checking authentication for public auth route:', pathname); - // Check if logout has recently occurred via query parameter const fromLogout = req.nextUrl.searchParams.get('fromLogout'); if (fromLogout === 'true') { - console.log( - 'Middleware: Logout detected via query param, bypassing session check for public auth route' - ); return NextResponse.next(); } @@ -119,23 +93,11 @@ export async function middleware(req: NextRequest) { data: { session }, } = await supabase.auth.getSession(); - console.log('Middleware: Public route session check:', { - hasSession: !!session, - hasUser: !!session?.user, - userId: session?.user?.id, - userEmail: session?.user?.email, - hasAccessToken: !!session?.access_token, - expiresAt: session?.expires_at, - pathname, - fromLogout, - }); - if (session?.user && session.user.id && session.access_token) { // Check if session is expired if (session.expires_at) { const now = Math.floor(Date.now() / 1000); if (now >= session.expires_at) { - console.log('Middleware: Session expired for public auth route'); return NextResponse.next(); } } @@ -148,28 +110,19 @@ export async function middleware(req: NextRequest) { error: userError, } = await supabase.auth.getUser(); if (userError || !user) { - console.log('Middleware: Session has invalid user, treating as logged out'); return NextResponse.next(); } // If we get here, the session is actually valid - console.log('Middleware: User authenticated, redirecting to dashboard'); return NextResponse.redirect(new URL('/dashboard', req.url)); } catch (userCheckError) { - console.log( - 'Middleware: Error checking user validity, treating as logged out:', - userCheckError - ); return NextResponse.next(); } - } else { - console.log('Middleware: No valid session found for public auth route'); } } return NextResponse.next(); } catch (error) { - console.error('Middleware error:', error); // On error, allow the request to proceed return NextResponse.next(); } diff --git a/src/utils/sync-stripe-products.ts b/src/utils/sync-stripe-products.ts index 7121c82..a194ac5 100644 --- a/src/utils/sync-stripe-products.ts +++ b/src/utils/sync-stripe-products.ts @@ -1,20 +1,81 @@ import { stripe } from '../lib/stripe'; import { upsertProductRecord, upsertPriceRecord } from '../lib/stripe/admin-tasks'; +import { logger } from './logger'; export async function syncStripeProductsAndPrices() { - console.log('\n\n syncing stripe products and prices'); + logger.info('Starting Stripe products and prices sync...'); + + // Check if Stripe is properly configured + if (!stripe) { + const error = 'Stripe client is not initialized'; + logger.error('❌ Stripe configuration error:', error); + throw new Error(error); + } + + // Check if Stripe secret key is available + if (!process.env.STRIPE_SECRET_KEY) { + const error = 'STRIPE_SECRET_KEY environment variable is not set'; + logger.error('❌ Stripe configuration error:', error); + throw new Error(error); + } + try { + logger.info('Fetching products from Stripe...'); const products = await stripe.products.list({ active: true, limit: 100 }); + logger.info(`Found ${products.data.length} active products in Stripe`); + + if (products.data.length === 0) { + logger.warn( + '⚠️ No active products found in Stripe. Please create products in your Stripe dashboard first.' + ); + return { success: true, productsCount: 0, message: 'No products to sync' }; + } + for (const product of products.data) { - await upsertProductRecord(product); - const prices = await stripe.prices.list({ product: product.id, active: true, limit: 100 }); - for (const price of prices.data) { - await upsertPriceRecord(price); + logger.info(`Syncing product: ${product.name} (${product.id})`); + try { + await upsertProductRecord(product); + logger.info(`βœ… Product synced: ${product.name}`); + } catch (productError) { + logger.error(`❌ Failed to sync product ${product.name}:`, productError); + // Continue with other products instead of failing completely + continue; + } + + try { + const prices = await stripe.prices.list({ product: product.id, active: true, limit: 100 }); + logger.info(`Found ${prices.data.length} active prices for product ${product.name}`); + + for (const price of prices.data) { + logger.info(`Syncing price: ${price.id} for product ${product.name}`); + try { + await upsertPriceRecord(price); + logger.info(`βœ… Price synced: ${price.id}`); + } catch (priceError) { + logger.error(`❌ Failed to sync price ${price.id}:`, priceError); + // Continue with other prices + continue; + } + } + } catch (pricesError) { + logger.error(`❌ Failed to fetch prices for product ${product.name}:`, pricesError); + // Continue with other products + continue; } } - console.log('βœ… Stripe products and prices synced successfully!'); + + logger.info('βœ… Stripe products and prices sync completed!'); + return { success: true, productsCount: products.data.length }; } catch (err) { - console.error('❌ Error syncing Stripe products/prices:', err); - process.exit(1); + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; + const errorStack = err instanceof Error ? err.stack : 'No stack trace available'; + + logger.error('❌ Error syncing Stripe products/prices:', { + message: errorMessage, + stack: errorStack, + error: err, + }); + + throw new Error(`Stripe sync failed: ${errorMessage}`); } }