├── settings.json ├── app ├── favicon.ico ├── favicon-vercel.ico ├── metadata.ts ├── api │ ├── stripe │ │ ├── test │ │ │ └── route.ts │ │ ├── reactivate │ │ │ └── route.ts │ │ ├── cancel │ │ │ └── route.ts │ │ ├── sync │ │ │ └── route.ts │ │ └── webhook │ │ │ └── route.ts │ └── user │ │ └── delete │ │ └── route.ts ├── layout.tsx ├── globals.css ├── auth │ └── callback │ │ └── route.ts ├── login │ └── page.tsx ├── verify-email │ └── page.tsx ├── pay │ └── page.tsx ├── reset-password │ └── page.tsx ├── update-password │ └── page.tsx ├── dashboard │ └── page.tsx ├── profile │ └── page.tsx └── page.tsx ├── public ├── Google-Logo.png ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── postcss.config.mjs ├── types ├── ValidateEntryTypes.ts └── stripe.d.ts ├── next.config.js ├── components ├── LoadingSpinner.tsx ├── MetricCard.tsx ├── BuyMeCoffee.tsx ├── PostHogErrorBoundary.tsx ├── TypewriterEffect.tsx ├── PostHogPageView.tsx ├── SubscriptionStatus.tsx ├── DemoWidget.tsx ├── VideoModal.tsx ├── StripeBuyButton.tsx ├── ForgotPasswordModal.tsx ├── AccountManagement.tsx ├── PricingSection.tsx ├── LoginForm.tsx ├── TopBar.tsx └── OnboardingTour.tsx ├── eslint.config.mjs ├── .eslintrc.json ├── utils ├── env.ts ├── analytics.ts ├── supabase-admin.ts ├── supabase.ts ├── posthog.ts └── cors.ts ├── .env.example ├── tsconfig.json ├── config └── api.ts ├── contexts ├── PostHogContext.tsx ├── ProtectedRoute.tsx └── AuthContext.tsx ├── next.config.ts ├── .cursor └── mcp.json.example ├── LICENSE ├── .gitignore ├── package.json ├── tailwind.config.ts ├── hooks ├── useTrialStatus.ts └── useSubscription.ts ├── initial_supabase_table_schema.sql └── README.md /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcp.enableLanguageServer": true, 3 | "mcp.servers": ["stripe", "supabase"] 4 | } -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShenSeanChen/launch-mvp-stripe-nextjs-supabase/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/favicon-vercel.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShenSeanChen/launch-mvp-stripe-nextjs-supabase/HEAD/app/favicon-vercel.ico -------------------------------------------------------------------------------- /public/Google-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShenSeanChen/launch-mvp-stripe-nextjs-supabase/HEAD/public/Google-Logo.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | 3 | export const metadata: Metadata = { 4 | title: "Sean Dev Template", 5 | description: "Your app description", 6 | }; -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /types/ValidateEntryTypes.ts: -------------------------------------------------------------------------------- 1 | export type ValidateEntry = { 2 | perplex_validate_test_id: string; // UUID type 3 | created_at: Date; // Timestamp type 4 | number_input: number; // Integer type 5 | }; -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | // ... your existing config ... 4 | webpack: (config, { isServer }) => { 5 | config.ignoreWarnings = [ 6 | { module: /node_modules\/punycode/ } 7 | ]; 8 | return config; 9 | }, 10 | } 11 | 12 | module.exports = nextConfig -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Loading spinner component for Suspense fallback 3 | * @file components/LoadingSpinner.tsx 4 | */ 5 | 6 | export default function LoadingSpinner() { 7 | return ( 8 |
9 |
10 |
11 | ); 12 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/no-unescaped-entities": "off", 5 | "@typescript-eslint/no-unused-vars": ["warn", { 6 | "varsIgnorePattern": "^_", 7 | "argsIgnorePattern": "^_" 8 | }], 9 | "@typescript-eslint/no-explicit-any": "warn", 10 | "react-hooks/rules-of-hooks": "warn", 11 | "react/jsx-no-comment-textnodes": "warn" 12 | }, 13 | "ignorePatterns": [ 14 | "node_modules/", 15 | ".next/", 16 | "public/" 17 | ] 18 | } -------------------------------------------------------------------------------- /utils/env.ts: -------------------------------------------------------------------------------- 1 | export function validateEnv() { 2 | const requiredEnvVars = [ 3 | 'NEXT_PUBLIC_SUPABASE_URL', 4 | 'NEXT_PUBLIC_SUPABASE_ANON_KEY', 5 | 'SUPABASE_SERVICE_ROLE_KEY', 6 | 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY', 7 | 'NEXT_PUBLIC_STRIPE_BUTTON_ID', 8 | 'STRIPE_SECRET_KEY', 9 | 'STRIPE_WEBHOOK_SECRET', 10 | 'NEXT_PUBLIC_APP_URL', 11 | ]; 12 | 13 | for (const envVar of requiredEnvVars) { 14 | if (!process.env[envVar]) { 15 | throw new Error(`Missing required environment variable: ${envVar}`); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /types/stripe.d.ts: -------------------------------------------------------------------------------- 1 | // The .d.ts extension is a convention that tells TypeScript that this file is purely for type declarations and shouldn't be compiled into JavaScript. 2 | 3 | // Import the Stripe namespace 4 | import Stripe from 'stripe'; 5 | 6 | // Extend the JSX namespace for the Stripe Buy Button 7 | declare namespace JSX { 8 | interface IntrinsicElements { 9 | 'stripe-buy-button': { 10 | 'buy-button-id': string; 11 | 'publishable-key': string; 12 | } 13 | } 14 | } 15 | 16 | // Export Stripe types if needed elsewhere 17 | export type StripeCheckoutSession = Stripe.Checkout.Session; 18 | export type StripeEvent = Stripe.Event; -------------------------------------------------------------------------------- /components/MetricCard.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface MetricCardProps { 4 | number: string; 5 | label: string; 6 | icon: ReactNode; 7 | } 8 | 9 | export const MetricCard = ({ number, label, icon }: MetricCardProps) => ( 10 |
11 | {/* Center icon */} 12 |
13 | {icon} 14 |
15 | {/* Center number and label */} 16 |
17 |
{number}
18 |
{label}
19 |
20 |
21 | ); -------------------------------------------------------------------------------- /utils/analytics.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import posthog from 'posthog-js' 4 | 5 | type EventProperties = { 6 | [key: string]: any 7 | } 8 | 9 | export const trackEvent = (eventName: string, properties?: EventProperties) => { 10 | try { 11 | if (posthog && posthog.capture) { 12 | posthog.capture(eventName, properties) 13 | } 14 | } catch (error) { 15 | console.error('Error tracking event:', error) 16 | } 17 | } 18 | 19 | export const identifyUser = (userId: string, properties?: EventProperties) => { 20 | try { 21 | if (posthog && posthog.identify) { 22 | posthog.identify(userId, properties) 23 | } 24 | } catch (error) { 25 | console.error('Error identifying user:', error) 26 | } 27 | } -------------------------------------------------------------------------------- /components/BuyMeCoffee.tsx: -------------------------------------------------------------------------------- 1 | import { Coffee } from 'lucide-react'; 2 | import { motion } from 'framer-motion'; 3 | 4 | export function BuyMeCoffee() { 5 | const COFFEE_URL = 'https://buy.stripe.com/5kA176bA895ggog4gh'; 6 | 7 | return ( 8 | 16 | 17 | Buy Me a Coffee 18 | 19 | ); 20 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL=http://localhost:8000 2 | NEXT_PUBLIC_API_URL=http://localhost:8080 3 | NEXT_PUBLIC_WS_URL=ws://localhost:8080 4 | 5 | # Supabase Configuration 6 | NEXT_PUBLIC_SUPABASE_URL= 7 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 8 | SUPABASE_SERVICE_ROLE_KEY= 9 | 10 | # OpenAI Configuration (you'll need to add your key) 11 | OPENAI_API_KEY= 12 | 13 | # Stripe Configuration 14 | # NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_ 15 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_ 16 | NEXT_PUBLIC_STRIPE_BUTTON_ID=buy_btn_ 17 | # STRIPE_SECRET_KEY=sk_test_ 18 | STRIPE_SECRET_KEY=sk_live_ 19 | # STRIPE_WEBHOOK_SECRET=whsec_ 20 | STRIPE_WEBHOOK_SECRET=whsec_ 21 | 22 | # ANALYTICS 23 | NEXT_PUBLIC_POSTHOG_KEY= 24 | NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com -------------------------------------------------------------------------------- /components/PostHogErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Component, ErrorInfo, ReactNode } from 'react' 4 | 5 | interface Props { 6 | children: ReactNode 7 | } 8 | 9 | interface State { 10 | hasError: boolean 11 | } 12 | 13 | export class PostHogErrorBoundary extends Component { 14 | public state: State = { 15 | hasError: false 16 | } 17 | 18 | public static getDerivedStateFromError(): State { 19 | return { hasError: true } 20 | } 21 | 22 | public componentDidCatch(error: Error, errorInfo: ErrorInfo) { 23 | console.error('PostHog Error:', error, errorInfo) 24 | } 25 | 26 | public render() { 27 | if (this.state.hasError) { 28 | return null 29 | } 30 | 31 | return this.props.children 32 | } 33 | } -------------------------------------------------------------------------------- /utils/supabase-admin.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | 3 | if (!process.env.NEXT_PUBLIC_SUPABASE_URL) { 4 | throw new Error('Missing env.NEXT_PUBLIC_SUPABASE_URL'); 5 | } 6 | if (!process.env.SUPABASE_SERVICE_ROLE_KEY) { 7 | throw new Error('Missing env.SUPABASE_SERVICE_ROLE_KEY'); 8 | } 9 | 10 | export const supabaseAdmin = createClient( 11 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 12 | process.env.SUPABASE_SERVICE_ROLE_KEY!, 13 | { 14 | auth: { 15 | autoRefreshToken: false, 16 | persistSession: false, 17 | detectSessionInUrl: false 18 | }, 19 | global: { 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | 'apikey': process.env.SUPABASE_SERVICE_ROLE_KEY!, 23 | } 24 | } 25 | } 26 | ); -------------------------------------------------------------------------------- /components/TypewriterEffect.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | interface TypewriterEffectProps { 4 | text: string; 5 | delay?: number; 6 | } 7 | 8 | export const TypewriterEffect = ({ text, delay = 50 }: TypewriterEffectProps) => { 9 | const [displayText, setDisplayText] = useState(''); 10 | const [currentIndex, setCurrentIndex] = useState(0); 11 | 12 | useEffect(() => { 13 | if (currentIndex < text.length) { 14 | const timeout = setTimeout(() => { 15 | setDisplayText(prev => prev + text[currentIndex]); 16 | setCurrentIndex(prev => prev + 1); 17 | }, delay); 18 | 19 | return () => clearTimeout(timeout); 20 | } 21 | }, [currentIndex, delay, text]); 22 | 23 | return {displayText}; 24 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | }, 24 | "typeRoots": ["./node_modules/@types", "./types"] 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "types/**/*.d.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /utils/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | 3 | if (!process.env.NEXT_PUBLIC_SUPABASE_URL) { 4 | throw new Error('Missing env.NEXT_PUBLIC_SUPABASE_URL'); 5 | } 6 | if (!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) { 7 | throw new Error('Missing env.NEXT_PUBLIC_SUPABASE_ANON_KEY'); 8 | } 9 | 10 | export const supabase = createClient( 11 | process.env.NEXT_PUBLIC_SUPABASE_URL, 12 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 13 | { 14 | auth: { 15 | persistSession: true, 16 | autoRefreshToken: true, 17 | }, 18 | global: { 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | 'Accept': 'application/json', 22 | 'apikey': process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 23 | }, 24 | }, 25 | db: { 26 | schema: 'public' 27 | } 28 | } 29 | ); -------------------------------------------------------------------------------- /config/api.ts: -------------------------------------------------------------------------------- 1 | // Directory: /config/api.ts 2 | 3 | /** 4 | * API configuration for the application 5 | */ 6 | export const API_CONFIG = { 7 | baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080', 8 | wsUrl: process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8080', 9 | endpoints: { 10 | health: '/', 11 | hello: '/api/v1/hello', 12 | ws: '/ws', 13 | processVoice: '/process-voice', 14 | setTimer: '/set-timer', 15 | }, 16 | } as const; 17 | 18 | /** 19 | * Get the full WebSocket URL 20 | */ 21 | export const getWsUrl = () => `${API_CONFIG.wsUrl}${API_CONFIG.endpoints.ws}`; 22 | 23 | /** 24 | * Get the full API URL for a given endpoint 25 | */ 26 | export const getApiUrl = (endpoint: keyof typeof API_CONFIG.endpoints) => 27 | `${API_CONFIG.baseUrl}${API_CONFIG.endpoints[endpoint]}`; 28 | 29 | -------------------------------------------------------------------------------- /contexts/PostHogContext.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { PostHogProvider as PHProvider } from 'posthog-js/react' 4 | import { useEffect, useState, useMemo } from 'react' 5 | import PostHogPageView from '@/components/PostHogPageView' 6 | import { initPostHog } from '@/utils/posthog' 7 | 8 | export function PostHogProvider({ children }: { children: React.ReactNode }) { 9 | const [isReady, setIsReady] = useState(false) 10 | 11 | const client = useMemo(() => { 12 | if (typeof window === 'undefined') return null 13 | return initPostHog() 14 | }, []) 15 | 16 | useEffect(() => { 17 | if (client) { 18 | setIsReady(true) 19 | } 20 | }, [client]) 21 | 22 | if (!isReady || !client) return null 23 | 24 | return ( 25 | 26 | 27 | {children} 28 | 29 | ) 30 | } -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | headers: async () => [ 5 | { 6 | source: '/:path*', 7 | headers: [ 8 | { 9 | key: 'X-Frame-Options', 10 | value: 'DENY', 11 | }, 12 | { 13 | key: 'X-Content-Type-Options', 14 | value: 'nosniff', 15 | }, 16 | { 17 | key: 'Referrer-Policy', 18 | value: 'strict-origin-when-cross-origin', 19 | }, 20 | { 21 | key: 'X-XSS-Protection', 22 | value: '1; mode=block', 23 | }, 24 | { 25 | key: 'Strict-Transport-Security', 26 | value: 'max-age=31536000; includeSubDomains', 27 | }, 28 | ], 29 | }, 30 | ], 31 | /* config options here */ 32 | }; 33 | 34 | export default nextConfig; 35 | -------------------------------------------------------------------------------- /.cursor/mcp.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "stripe": { 4 | "command": "npx", 5 | "args": [ 6 | "-y", 7 | "@stripe/mcp" 8 | ], 9 | "env": { 10 | "STRIPE_SECRET_KEY": "your_stripe_test_key_here" 11 | } 12 | }, 13 | "supabase": { 14 | "command": "npx", 15 | "args": [ 16 | "-y", 17 | "@supabase/mcp-server-supabase@latest", 18 | "--access-token", 19 | "your_supabase_access_token_here" 20 | ] 21 | }, 22 | "github": { 23 | "command": "docker", 24 | "args": [ 25 | "run", 26 | "-i", 27 | "--rm", 28 | "-e", 29 | "GITHUB_PERSONAL_ACCESS_TOKEN", 30 | "ghcr.io/github/github-mcp-server" 31 | ], 32 | "env": { 33 | "GITHUB_PERSONAL_ACCESS_TOKEN": "your_github_personal_access_token_here" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /components/PostHogPageView.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { usePathname, useSearchParams } from "next/navigation" 4 | import { useEffect, Suspense } from "react" 5 | import { usePostHog } from 'posthog-js/react' 6 | 7 | function PostHogPageView() { 8 | const pathname = usePathname() 9 | const searchParams = useSearchParams() 10 | const posthog = usePostHog() 11 | 12 | useEffect(() => { 13 | if (pathname && posthog) { 14 | let url = window.origin + pathname 15 | if (searchParams.toString()) { 16 | url = url + `?${searchParams.toString()}` 17 | } 18 | posthog.capture('$pageview', { '$current_url': url }) 19 | } 20 | }, [pathname, searchParams, posthog]) 21 | 22 | return null 23 | } 24 | 25 | export default function SuspendedPostHogPageView() { 26 | return ( 27 | 28 | 29 | 30 | ) 31 | } -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/SubscriptionStatus.tsx: -------------------------------------------------------------------------------- 1 | import { useSubscription } from '@/hooks/useSubscription'; 2 | import { useRouter } from 'next/navigation'; 3 | 4 | export function SubscriptionStatus() { 5 | const { subscription, isLoading, error } = useSubscription(); 6 | const router = useRouter(); 7 | 8 | if (isLoading) { 9 | return
Checking subscription status...
; 10 | } 11 | 12 | if (error) { 13 | return
Error checking subscription: {error}
; 14 | } 15 | 16 | if (subscription?.status === 'active' || subscription?.status === 'trialing') { 17 | return ( 18 |
19 |
20 | You have an active subscription! 21 |
22 | 28 |
29 | ); 30 | } 31 | 32 | return null; 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ShenSeanChen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | .idea/ 27 | .vscode/ 28 | *.swp 29 | *.swo 30 | 31 | # debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | .pnpm-debug.log* 36 | 37 | # local env files 38 | .env 39 | .env*.local 40 | .env.development 41 | .env.test 42 | .env.production 43 | .env.local 44 | .env.development.local 45 | .env.test.local 46 | .env.production.local 47 | 48 | # vercel 49 | .vercel 50 | 51 | # typescript 52 | *.tsbuildinfo 53 | next-env.d.ts 54 | 55 | # Supabase 56 | **/supabase/.temp 57 | .supabase/ 58 | 59 | # Security and keys 60 | *.key 61 | *.pem 62 | *.cert 63 | *.crt 64 | *.p12 65 | *.pfx 66 | *.keystore 67 | 68 | # Logs 69 | logs 70 | *.log 71 | 72 | # Cache directories 73 | .cache/ 74 | .next/cache/ 75 | 76 | # Cursor 77 | .cursor/mcp.json 78 | -------------------------------------------------------------------------------- /utils/posthog.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import posthog from 'posthog-js' 4 | 5 | let posthogClient: typeof posthog | null = null 6 | 7 | // Initialize PostHog with better error handling 8 | export const initPostHog = () => { 9 | if (posthogClient) return posthogClient 10 | 11 | try { 12 | if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) { 13 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { 14 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com', 15 | loaded: (posthog) => { 16 | if (process.env.NODE_ENV === 'development') posthog.debug(false) 17 | }, 18 | autocapture: false, // Disable autocapture for better performance 19 | capture_pageview: false, // We'll handle this manually 20 | persistence: 'localStorage', 21 | cross_subdomain_cookie: false 22 | }) 23 | posthogClient = posthog 24 | } 25 | } catch (error) { 26 | // Fail silently in production, log in development 27 | if (process.env.NODE_ENV === 'development') { 28 | console.error('PostHog initialization error:', error) 29 | } 30 | } 31 | 32 | return posthogClient 33 | } -------------------------------------------------------------------------------- /app/api/stripe/test/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import type { NextRequest } from 'next/server'; 3 | import Stripe from 'stripe'; 4 | import { withCors } from '@/utils/cors'; 5 | 6 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | export const GET = withCors(async function GET(request: NextRequest) { 10 | try { 11 | console.log('Testing Stripe connection...'); 12 | console.log('Stripe key starts with:', process.env.STRIPE_SECRET_KEY?.substring(0, 8) + '...'); 13 | 14 | // Just verify the connection works 15 | await stripe.balance.retrieve(); 16 | console.log('Stripe connection successful'); 17 | 18 | return NextResponse.json({ 19 | status: 'success', 20 | message: 'Stripe connection successful', 21 | keyPrefix: process.env.STRIPE_SECRET_KEY?.substring(0, 8) + '...' 22 | }); 23 | } catch (error) { 24 | console.error('Stripe test failed:', error); 25 | return NextResponse.json({ 26 | status: 'error', 27 | message: error instanceof Error ? error.message : 'Unknown error' 28 | }, { status: 500 }); 29 | } 30 | }); -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Geist } from "next/font/google"; 4 | import "./globals.css"; 5 | import { AuthProvider } from '@/contexts/AuthContext'; 6 | import TopBar from '../components/TopBar'; 7 | import ProtectedRoute from '@/contexts/ProtectedRoute'; 8 | import { Analytics } from "@vercel/analytics/react" 9 | // import { PostHogProvider } from '@/contexts/PostHogContext'; 10 | // import { PostHogErrorBoundary } from '@/components/PostHogErrorBoundary'; 11 | 12 | const geist = Geist({ subsets: ['latin'] }); 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | {/* 24 | */} 25 | 26 | 27 | 28 |
{children}
29 |
30 |
31 | {/*
32 |
*/} 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #F8F9FA; 7 | --foreground: #2F3E46; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #1A1B26; 13 | --foreground: #F8F9FA; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: 'Inter', sans-serif; 21 | } 22 | 23 | /* Add some cooking-themed utility classes */ 24 | @layer components { 25 | .recipe-card { 26 | @apply bg-white dark:bg-neutral-dark rounded-lg shadow-sm border border-primary/10 dark:border-primary/20 transition-all duration-300 hover:shadow-md; 27 | } 28 | 29 | .cooking-button { 30 | @apply bg-primary hover:bg-primary-dark text-white rounded-lg transition-all duration-300 transform hover:scale-102 active:scale-98; 31 | } 32 | 33 | .accent-button { 34 | @apply bg-secondary hover:bg-secondary-dark text-white rounded-lg transition-all duration-300; 35 | } 36 | } 37 | 38 | /* Hide scrollbar styles */ 39 | .hide-scrollbar::-webkit-scrollbar { 40 | display: none; 41 | } 42 | 43 | .hide-scrollbar { 44 | -ms-overflow-style: none; 45 | scrollbar-width: none; 46 | } -------------------------------------------------------------------------------- /components/DemoWidget.tsx: -------------------------------------------------------------------------------- 1 | export const DemoWidget = () => { 2 | return ( 3 |
4 |

Try It Out

5 |
6 |
7 |
8 | 11 |