diff --git a/.cursor/commands/dashboard.md b/.cursor/commands/dashboard.md new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/db-and-i18n.yml b/.github/workflows/db-and-i18n.yml new file mode 100644 index 0000000..63bf4e0 --- /dev/null +++ b/.github/workflows/db-and-i18n.yml @@ -0,0 +1,86 @@ +name: DB migrations & i18n (minimal) + +on: + push: + branches: [ dev, main ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + preview: + if: github.ref == 'refs/heads/dev' + runs-on: ubuntu-latest + environment: preview + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + steps: + - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.17.1 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + - name: Ensure pnpm store exists (for caching) + run: mkdir -p "$(pnpm store path)" + - run: corepack enable + - run: pnpm install --no-frozen-lockfile + - name: Lint + run: pnpm lint + # pgcrypto is ensured inside setup.js using node-postgres + - name: Run migrations + run: node setup.js + - name: i18n check + run: pnpm run i18n:check + - name: Brand detection tests + run: pnpm run test:brand-detection + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + - name: Trigger Vercel Deploy Hook + if: ${{ success() }} + run: curl -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_URL }} + + production: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: production + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + steps: + - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.17.1 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + - name: Ensure pnpm store exists (for caching) + run: mkdir -p "$(pnpm store path)" + - run: corepack enable + - run: pnpm install --no-frozen-lockfile + - name: Lint + run: pnpm lint + # pgcrypto is ensured inside setup.js using node-postgres + - name: Run migrations + run: node setup.js + - name: i18n check + run: pnpm run i18n:check + - name: Brand detection tests + run: pnpm run test:brand-detection + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + - name: Trigger Vercel Deploy Hook + if: ${{ success() }} + run: curl -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_URL }} + + diff --git a/.gitignore b/.gitignore index c924c45..c779278 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ cookies.txt # Generated files repomix-output.txt i18n.cache + +# lockfiles from other package managers +package-lock.json \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..98eb310 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `app/` hosts Next.js 15 routes, server actions, and layouts; keep features grouped by route segment. +- `components/` contains shared UI; export reusable pieces via `index.ts` barrels where helpful and name files in PascalCase. +- `lib/` centralizes utilities (`auth`, `clients`, `validators`); prefer the `@/` alias when importing. +- `config/`, `hooks/`, `i18n/`, and `messages/` hold environment loaders, React hooks, and localization strings; sync locale changes with `pnpm i18n:check`. +- `supabase/`, `migrations/`, and `better-auth_migrations/` track SQL; commit generated files and keep IDs aligned with drizzle migrations. +- `public/` stores static assets; `scripts/` houses automation TSX scripts; `.github/workflows/` defines CI/CD. + +## + +## Build, Test, and Development Commands +- `pnpm run setup` bootstraps the project (installs deps, seeds auth tables). +- `pnpm dev` runs the Turbopack dev server on localhost. +- `pnpm build` produces the production bundle; `pnpm start` serves the compiled app. +- `pnpm lint` enforces ESLint+Next rules; run before every PR. +- `pnpm db:generate`, `pnpm db:migrate`, and `pnpm db:push` manage drizzle migrations; `pnpm db:studio` opens the schema explorer. +- `pnpm debug:env` prints resolved environment variables for troubleshooting. + +## Coding Style & Naming Conventions +TypeScript is strict; enable editors to use the repo `tsconfig.json`. Rely on Prettier defaults (2-space indent, semicolons) and Next linting. Components and hooks use PascalCase and `useCamelCase` respectively; route folders stay lower-case kebab. Keep database files snake_case with numeric prefixes (`001_*`). Import with the `@/` alias instead of deep relative paths. Update localization keys consistently across `messages/` locales. + +## Development Guidelines +1. **Document every change** + - Update relevant `README`, comments, or inline docs when modifying or creating features. + - When in doubt, explain *why* the code exists. + +2. **No hard-coded strings** + - Always use `next-intl` and the `i18n` system. + - Add/modify entries in `/messages` and ensure both locales are updated. + - Run `pnpm i18n:check` before committing. + +3. **Prefer reuse over duplication** + - Before writing new code, check if a utility, hook, or component already implements similar behavior. + - Extend existing logic rather than duplicating functionality. + +4. **Avoid rigid static logic** + - Do not over-rely on long `if/else` or `switch` blocks with hard-coded cases. + - Prefer flexible patterns, data-driven configs, or leverage an **LLM-powered function** when appropriate for broader coverage and adaptability. + +## Testing Guidelines +Automated tests are not yet wired; guard changes with `pnpm lint`, manual verification in `pnpm dev`, and database smoke tests via `pnpm db:studio`. When adding tests, colocate `*.test.ts(x)` beside the feature and stub network calls to keep runs deterministic. Extend CI to run new test suites before merging. + +## Commit & Pull Request Guidelines +Write imperative, concise commit messages (~60 chars). Follow the current history by prefacing fixes with `fix:` when appropriate and omitting prefixes for general additions. Each PR should include: a short summary, linked issue or ticket, screenshots/GIFs for UI changes, notes on env vars or migrations, and a checklist of commands run (`pnpm lint`, relevant db tasks). Request review once CI passes and docs/config updates land in the same branch. + +## Environment & Secrets +Copy `.env.example` to `.env.local` and fill the required keys: `DATABASE_URL`, `BETTER_AUTH_SECRET`, email providers, and AI API keys as needed. Use `pnpm debug:env` to confirm values. Never commit `.env.local`; rely on Vercel project settings for deployment. diff --git a/README.md b/README.md index e942f3f..e7a3125 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# FireGEO Open-Source SaaS Starter +# VOXUM -FireGEO Demo +Voxum Get your SaaS running in minutes with authentication, billing, AI chat, and brand monitoring. Zero-config setup with Next.js 15, TypeScript, and PostgreSQL. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..a6068c4 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,140 @@ +# Guide de Dépannage - Voxum + +## Problème : Divergence entre ordinateurs + +Si vous obtenez des réponses sur un ordinateur mais pas sur l'autre, voici les causes les plus probables et leurs solutions : + +### 1. Variables d'environnement manquantes + +**Symptôme :** Aucune donnée n'est extraite, erreurs dans la console + +**Solution :** +```bash +# Exécuter le diagnostic +npm run debug:env +``` + +**Variables critiques à vérifier :** +- `FIRECRAWL_API_KEY` - **ESSENTIEL** pour le scraping +- `OPENAI_API_KEY` ou `ANTHROPIC_API_KEY` - **ESSENTIEL** pour l'extraction de données +- `AUTUMN_SECRET_KEY` - **ESSENTIEL** pour la vérification des crédits + +### 2. Configuration des providers AI + +**Symptôme :** Erreur "No AI providers configured" + +**Solution :** +1. Vérifiez que au moins une clé API AI est configurée +2. Redémarrez le serveur après avoir ajouté les variables +3. Vérifiez que les clés sont valides + +### 3. Problèmes de réseau + +**Symptôme :** Timeouts, erreurs de connexion + +**Solutions :** +- Vérifiez votre connexion internet +- Vérifiez les paramètres de firewall/proxy +- Essayez avec une URL différente (plus simple) + +### 4. Problèmes de base de données + +**Symptôme :** Erreurs d'authentification, problèmes de crédits + +**Solution :** +```bash +# Vérifier la connexion à la base de données +npm run db:push +``` + +### 5. Problèmes de cache + +**Symptôme :** Données obsolètes ou incohérentes + +**Solution :** +```bash +# Nettoyer le cache et redémarrer +rm -rf .next +npm run dev +``` + +## Diagnostic Automatique + +### Exécuter le diagnostic complet + +```bash +npm run debug:env +``` + +Ce script vérifie : +- ✅ Variables d'environnement requises +- ✅ Configuration des providers AI +- ✅ Connexion à la base de données +- ✅ Connexion à Firecrawl +- ✅ Configuration des clés API + +### Logs de debug + +Les logs détaillés sont maintenant activés. Regardez la console pour : +- `🔍 [Scraper]` - Logs de scraping +- `🔍 [Processor]` - Logs d'extraction de données +- `🔍 [Scrape API]` - Logs de l'API +- `❌` - Erreurs détaillées + +## Solutions par Type d'Erreur + +### Erreur : "FIRECRAWL_API_KEY not configured" +```bash +# Ajouter dans .env.local +FIRECRAWL_API_KEY=your_key_here +``` + +### Erreur : "No AI providers configured" +```bash +# Ajouter au moins une clé API dans .env.local +OPENAI_API_KEY=your_key_here +# OU +ANTHROPIC_API_KEY=your_key_here +``` + +### Erreur : "Insufficient credits" +- Vérifiez votre compte Autumn +- Assurez-vous que les produits sont correctement configurés +- Vérifiez que `AUTUMN_SECRET_KEY` est correct + +### Erreur : "Connection timeout" +- Vérifiez votre connexion internet +- Essayez avec une URL plus simple +- Vérifiez les paramètres de proxy/firewall + +## Vérification Rapide + +1. **Variables d'environnement :** + ```bash + npm run debug:env + ``` + +2. **Redémarrer le serveur :** + ```bash + npm run dev + ``` + +3. **Tester avec une URL simple :** + - Essayez `https://example.com` d'abord + - Puis testez avec votre URL cible + +4. **Vérifier les logs :** + - Ouvrez la console du navigateur + - Regardez les logs du serveur + - Identifiez le point de défaillance + +## Support + +Si le problème persiste : +1. Exécutez `npm run debug:env` et partagez le résultat +2. Partagez les logs de la console +3. Indiquez l'URL que vous essayez de scraper +4. Précisez les différences entre les deux ordinateurs + + + diff --git a/app/autumn-verify/page.tsx b/app/[locale]/autumn-verify/page.tsx similarity index 94% rename from app/autumn-verify/page.tsx rename to app/[locale]/autumn-verify/page.tsx index 27fd328..cb95d67 100644 --- a/app/autumn-verify/page.tsx +++ b/app/[locale]/autumn-verify/page.tsx @@ -3,9 +3,10 @@ import { useCustomer } from '@/hooks/useAutumnCustomer'; import { useSession } from '@/lib/auth-client'; import { useEffect } from 'react'; +import type { Session } from 'better-auth'; // Separate component that uses Autumn hooks -function AutumnVerifyContent({ session }: { session: any }) { +function AutumnVerifyContent({ session }: { session: Session | null }) { const { customer, isLoading, error } = useCustomer(); useEffect(() => { @@ -73,7 +74,7 @@ function AutumnVerifyContent({ session }: { session: any }) {

Next Steps:

    -
  1. Check the browser console for the logged "Autumn Customer" object
  2. +
  3. Check the browser console for the logged "Autumn Customer" object
  4. Verify in your Autumn dashboard at https://app.useautumn.com/sandbox/customers that the customer was created
  5. The customer ID in Autumn should match your auth user ID: {session?.user?.id || 'Not logged in'}
diff --git a/app/[locale]/brand-monitor/page.tsx b/app/[locale]/brand-monitor/page.tsx new file mode 100644 index 0000000..1b4fe53 --- /dev/null +++ b/app/[locale]/brand-monitor/page.tsx @@ -0,0 +1,270 @@ +'use client'; + +import { BrandMonitor } from '@/components/brand-monitor/brand-monitor'; +import { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import { useTranslations } from 'next-intl'; +import { Menu, X, Plus, Trash2, Loader2 } from 'lucide-react'; +import { useCustomer, useRefreshCustomer } from '@/hooks/useAutumnCustomer'; +import { useBrandAnalyses, useBrandAnalysis, useDeleteBrandAnalysis } from '@/hooks/useBrandAnalyses'; +import { useCreditsInvalidation } from '@/hooks/useCreditsInvalidation'; +import { Button } from '@/components/ui/button'; +import type { BrandAnalysisWithSources } from '@/lib/db/schema'; +import { format } from 'date-fns'; +import { useSession } from '@/lib/auth-client'; +import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'; + +// Removed direct Product typing to accommodate varying shapes from Autumn + +// Separate component that uses Autumn hooks +function BrandMonitorContent() { + const router = useRouter(); + useParams(); + const t = useTranslations(); + const { customer, error } = useCustomer(); + const refreshCustomer = useRefreshCustomer(); + const { invalidateCredits } = useCreditsInvalidation(); + const [sidebarOpen, setSidebarOpen] = useState(true); + const [selectedAnalysisId, setSelectedAnalysisId] = useState(null); + const [resetCount, setResetCount] = useState(0); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [analysisToDelete, setAnalysisToDelete] = useState(null); + + // Queries and mutations + const { data: analyses, isLoading: analysesLoading } = useBrandAnalyses(); + const { data: currentAnalysis } = useBrandAnalysis(selectedAnalysisId); + const deleteAnalysis = useDeleteBrandAnalysis(); + const analysesList: BrandAnalysisWithSources[] = analyses ?? []; + const renderAnalysisItem = ( + item: BrandAnalysisWithSources + ): ReactElement => { + return ( +
setSelectedAnalysisId(item.id)} + > +
+
+

+ {item.companyName || t('brandMonitor.untitledAnalysis')} +

+

+ {item.url} +

+

+ {item.createdAt && format(new Date(item.createdAt), 'MMM d, yyyy')} +

+
+ +
+
+ ); + } + + // Get credits from customer data + const messageUsage = customer?.features?.credits; + const credits = messageUsage ? (messageUsage.balance || 0) : 0; + + // Determine active plan name/id + // Autumn customer products can have different shapes; use safe accessors + const products = (customer?.products ?? []) as unknown[]; + const getStatus = (p: unknown): string | undefined => { + if (p && typeof p === 'object' && 'status' in p) { + const s = (p as Record).status; + return typeof s === 'string' ? s : undefined; + } + return undefined; + }; + const getNameOrId = (p: unknown): string | undefined => { + if (p && typeof p === 'object') { + const obj = p as Record; + const name = typeof obj.name === 'string' ? obj.name : undefined; + const id = typeof obj.id === 'string' ? obj.id : undefined; + return name ?? id; + } + return undefined; + }; + const activeProduct = products.find((p) => { + const s = getStatus(p); + return s === 'active' || s === 'trialing' || s === 'past_due'; + }) ?? products[0]; + // DEV-only plan override via footer select + const [devPlanOverride, setDevPlanOverride] = useState(null); + useEffect(() => { + if (process.env.NODE_ENV !== 'development') return; + const read = () => { + try { + const v = localStorage.getItem('devPlanOverride'); + setDevPlanOverride(v); + } catch {} + }; + read(); + const handler = () => read(); + window.addEventListener('dev-plan-override-changed', handler); + return () => window.removeEventListener('dev-plan-override-changed', handler); + }, []); + + const effectivePlanName: string = ((devPlanOverride && process.env.NODE_ENV === 'development') + ? devPlanOverride + : (getNameOrId(activeProduct) || '')) as string; + const activePlanName: string = effectivePlanName; + const isStartPlan = activePlanName.toLowerCase().includes('start') || activePlanName.toLowerCase().includes('free'); + + useEffect(() => { + // If there's an auth error, redirect to login + if (error?.code === 'UNAUTHORIZED' || error?.code === 'AUTH_ERROR') { + router.push('/login'); + } + }, [error, router]); + + const handleCreditsUpdate = async () => { + // Use the global refresh to update customer data everywhere + await refreshCustomer(); + // Also invalidate React Query cache for credits + await invalidateCredits(); + }; + + const handleDeleteAnalysis = async (analysisId: string) => { + setAnalysisToDelete(analysisId); + setDeleteDialogOpen(true); + }; + + const confirmDelete = async () => { + if (analysisToDelete) { + await deleteAnalysis.mutateAsync(analysisToDelete); + if (selectedAnalysisId === analysisToDelete) { + setSelectedAnalysisId(null); + } + setAnalysisToDelete(null); + } + }; + + const handleNewAnalysis = () => { + logger.info('🆕 [BrandMonitorPage] New Analysis button clicked'); + setSelectedAnalysisId(null); + setResetCount((c) => c + 1); + }; + + return ( +
+
+ {/* Sidebar Toggle Button - Always visible */} + + + {/* Sidebar */} +
+
+ +
+ +
+ {analysesLoading ? ( +
{t('brandMonitor.loadingAnalyses')}
+ ) : analysesList.length === 0 ? ( +
{t('brandMonitor.noAnalysesYet')}
+ ) : ( +
+ {analysesList.map(renderAnalysisItem)} +
+ )} +
+
+ + {/* Main Content */} +
+
+ { + // This will be called when analysis completes + // We'll implement this in the next step + }} + // UI gating by plan + hideSourcesTab={isStartPlan} + hideWebSearchSources={isStartPlan} + /> +
+
+
+ + +
+ ); +} + +import { logger } from '@/lib/logger'; + +export default function BrandMonitorPage() { + const { data: session, isPending } = useSession(); + const t = useTranslations(); + const [isClient, setIsClient] = useState(false); + + // Prevent hydration mismatch by ensuring client-side rendering + useEffect(() => { + setIsClient(true); + }, []); + + // Show loading state during hydration and session check + if (!isClient || isPending) { + return ( +
+ +
+ ); + } + + if (!session) { + return ( +
+
+

{t('brandMonitor.pleaseLogIn')}

+
+
+ ); + } + + return ; +} \ No newline at end of file diff --git a/app/chat/page.tsx b/app/[locale]/chat/page.tsx similarity index 82% rename from app/chat/page.tsx rename to app/[locale]/chat/page.tsx index 22cd81b..cd2f7d6 100644 --- a/app/chat/page.tsx +++ b/app/[locale]/chat/page.tsx @@ -2,7 +2,8 @@ import { useState, useEffect } from 'react'; import { useSession } from '@/lib/auth-client'; -import { useRouter } from 'next/navigation'; +import { useRouter, useParams } from 'next/navigation'; +import { useTranslations } from 'next-intl'; import { useCustomer } from '@/hooks/useAutumnCustomer'; import { Button } from '@/components/ui/button'; import { Send, Menu, X, MessageSquare, Plus, Trash2 } from 'lucide-react'; @@ -10,20 +11,24 @@ import { useConversations, useConversation, useDeleteConversation } from '@/hook import { useSendMessage } from '@/hooks/useMessages'; import { format } from 'date-fns'; import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'; +import type { Session } from 'better-auth'; // Separate component that uses Autumn hooks -function ChatContent({ session }: { session: any }) { +function ChatContent({ session }: { session: Session }) { const router = useRouter(); + const params = useParams(); + const locale = params.locale as string; + const t = useTranslations(); const { allowed, customer, refetch } = useCustomer(); const [input, setInput] = useState(''); const [sidebarOpen, setSidebarOpen] = useState(true); - const [selectedConversationId, setSelectedConversationId] = useState(null); + const [selectedConversationId, setSelectedConversationId] = useState(undefined); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [conversationToDelete, setConversationToDelete] = useState(null); + const [conversationToDelete, setConversationToDelete] = useState(undefined); // Queries and mutations const { data: conversations, isLoading: conversationsLoading } = useConversations(); - const { data: currentConversation } = useConversation(selectedConversationId); + const { data: currentConversation } = useConversation(selectedConversationId ?? null); const sendMessage = useSendMessage(); const deleteConversation = useDeleteConversation(); @@ -58,13 +63,13 @@ function ChatContent({ session }: { session: any }) { // Refetch customer data to update credits in navbar await refetch(); - } catch (error: any) { + } catch (error) { console.error('Failed to send message:', error); } }; const handleNewConversation = () => { - setSelectedConversationId(null); + setSelectedConversationId(undefined); }; const handleDeleteConversation = async (conversationId: string) => { @@ -76,9 +81,9 @@ function ChatContent({ session }: { session: any }) { if (conversationToDelete) { await deleteConversation.mutateAsync(conversationToDelete); if (selectedConversationId === conversationToDelete) { - setSelectedConversationId(null); + setSelectedConversationId(undefined); } - setConversationToDelete(null); + setConversationToDelete(undefined); } }; @@ -92,15 +97,15 @@ function ChatContent({ session }: { session: any }) { className="w-full btn-firecrawl-orange" > - New Chat + {t('chat.newChat')}
{conversationsLoading ? ( -
Loading conversations...
+
{t('chat.loadingConversations')}
) : conversations?.length === 0 ? ( -
No conversations yet
+
{t('chat.noConversations')}
) : (
{conversations?.map((conversation) => ( @@ -114,10 +119,10 @@ function ChatContent({ session }: { session: any }) {

- {conversation.title || 'Untitled Conversation'} + {conversation.title || t('chat.untitledConversation')}

- {conversation.lastMessageAt && format(new Date(conversation.lastMessageAt), 'MMM d, h:mm a')} + {conversation.lastMessageAt ? format(new Date(conversation.lastMessageAt as unknown as string | number), 'MMM d, h:mm a') : ''}

@@ -158,7 +163,7 @@ function ChatContent({ session }: { session: any }) { {sidebarOpen ? : }

- {currentConversation?.title || 'New Conversation'} + {currentConversation?.title || t('chat.newConversation')}

@@ -169,27 +174,27 @@ function ChatContent({ session }: { session: any }) {
-

Loading your account data...

+

{t('chat.loadingAccountData')}

) : !hasMessages ? (
-

Credit-Based Messaging

+

{t('chat.creditBasedMessaging')}

- This is a demonstration of the credit-based messaging system. Each message consumes credits from your account balance. + {t('chat.creditBasedDesc')}

- You currently have {remainingMessages} message credits available. + {t('chat.youCurrentlyHave')} {remainingMessages} {t('chat.messageCreditsAvailable')}

@@ -211,7 +216,7 @@ function ChatContent({ session }: { session: any }) {

- {format(new Date(message.createdAt), 'h:mm a')} + {message.createdAt ? format(new Date(message.createdAt as unknown as string | number), 'h:mm a') : ''}

@@ -232,9 +237,9 @@ function ChatContent({ session }: { session: any }) {
-

Start a Conversation

+

{t('chat.startConversation')}

- Send a message to begin chatting with AI + {t('chat.sendMessageToBegin')}

@@ -254,7 +259,7 @@ function ChatContent({ session }: { session: any }) { type="text" value={input} onChange={(e) => setInput(e.target.value)} - placeholder={hasMessages ? "Type your message..." : "No messages available"} + placeholder={hasMessages ? t('chat.typeMessage') : t('chat.noMessagesAvailable')} disabled={!hasMessages || sendMessage.isPending} className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 disabled:bg-gray-100 disabled:text-gray-500" /> @@ -272,10 +277,10 @@ function ChatContent({ session }: { session: any }) { diff --git a/app/dashboard/page.tsx b/app/[locale]/dashboard/page.tsx similarity index 52% rename from app/dashboard/page.tsx rename to app/[locale]/dashboard/page.tsx index 72fe56a..a0db657 100644 --- a/app/dashboard/page.tsx +++ b/app/[locale]/dashboard/page.tsx @@ -1,19 +1,27 @@ 'use client'; -import { useCustomer, usePricingTable } from 'autumn-js/react'; +import { useCustomer } from '@/hooks/useAutumnCustomer'; +import { usePricingTable } from 'autumn-js/react'; +import { Product } from 'autumn-js'; import { useSession } from '@/lib/auth-client'; -import { useRouter } from 'next/navigation'; +import { useRouter, useParams } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { Lock, CheckCircle, AlertCircle, Loader2, User, Mail, Phone, Edit2, Save, X } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { Lock, CheckCircle, Loader2, User, Mail, Phone, Edit2, Save, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import ProductChangeDialog from '@/components/autumn/product-change-dialog'; import { useProfile, useUpdateProfile, useSettings, useUpdateSettings } from '@/hooks/useProfile'; +// Infer the session type from useSession +type SessionData = NonNullable['data']>; + // Separate component that uses Autumn hooks -function DashboardContent({ session }: { session: any }) { - const { customer, attach } = useCustomer(); +function DashboardContent({ session }: { session: SessionData }) { + const { customer, attach, refetch } = useCustomer(); const { products } = usePricingTable(); const [loadingProductId, setLoadingProductId] = useState(null); + const t = useTranslations(); + useParams(); // Profile and settings hooks const { data: profileData } = useProfile(); @@ -77,10 +85,13 @@ function DashboardContent({ session }: { session: any }) { await attach({ productId, dialog: ProductChangeDialog, - returnUrl: window.location.origin + '/dashboard', - successUrl: window.location.origin + '/dashboard', - cancelUrl: window.location.origin + '/dashboard', + checkoutSessionParams: { + success_url: window.location.origin + '/dashboard', + cancel_url: window.location.origin + '/dashboard', + }, }); + // Refresh customer data after product change + await refetch(); } finally { setLoadingProductId(null); } @@ -89,12 +100,12 @@ function DashboardContent({ session }: { session: any }) { return (
-

Dashboard

+

{t('dashboard.title')}

{/* Profile Section */}
-

Profile Information

+

{t('dashboard.profileInformation')}

{!isEditingProfile ? ( ) : (
@@ -113,7 +124,7 @@ function DashboardContent({ session }: { session: any }) { disabled={updateProfile.isPending} > - Save + {t('dashboard.save')}
)} @@ -132,7 +143,7 @@ function DashboardContent({ session }: { session: any }) {

{session.user?.email}

@@ -140,7 +151,7 @@ function DashboardContent({ session }: { session: any }) {
{isEditingProfile ? ( setProfileForm({ ...profileForm, displayName: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Enter your display name" + placeholder={t('dashboard.enterDisplayName')} /> ) : (

- {profileData?.profile?.displayName || 'Not set'} + {profileData?.profile?.displayName || t('dashboard.notSet')}

)}
@@ -160,7 +171,7 @@ function DashboardContent({ session }: { session: any }) {
{isEditingProfile ? ( setProfileForm({ ...profileForm, phone: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Enter your phone number" + placeholder={t('dashboard.enterPhone')} /> ) : (

- {profileData?.profile?.phone || 'Not set'} + {profileData?.profile?.phone || t('dashboard.notSet')}

)}
{isEditingProfile ? (