diff --git a/lander/app/dashboard/page.tsx b/lander/app/dashboard/page.tsx deleted file mode 100644 index e05e92420..000000000 --- a/lander/app/dashboard/page.tsx +++ /dev/null @@ -1,586 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import Navigation from '../components/Navigation'; -import { useCurrentSubscriptionPlan } from '../hooks/useCurrentSubscriptionPlan'; -import { useStripePlans } from '../hooks/useStripePlans'; -import { useStripeCheckout } from '../hooks/useStripeCheckout'; -import { useCoinbaseCheckout } from '../hooks/useCoinbaseCheckout'; - -// Mock data - in production, this would come from an API -const mockUsage = { - dailyRecapsUsed: 8, - dailyRecapsLimit: 15, - aiTokensUsed: 125000, - aiTokensLimit: 500000, - connectionsUsed: 2, - connectionsLimit: 3, -}; - -export default function Dashboard() { - const { plan, loading, error, refetch } = useCurrentSubscriptionPlan(); - const { plans: stripePlans, loading: plansLoading } = useStripePlans(); - const { handleCheckout: handleStripeCheckout, loading: stripeLoading } = useStripeCheckout(); - const { handleCheckout: handleCoinbaseCheckout, loading: coinbaseLoading } = useCoinbaseCheckout(); - const [showCancelModal, setShowCancelModal] = useState(false); - const [showConvertModal, setShowConvertModal] = useState(false); - const [showSwitchModal, setShowSwitchModal] = useState(false); - const [selectedPlan, setSelectedPlan] = useState(null); - const [billingCycle, setBillingCycle] = useState<'monthly' | 'annual'>('monthly'); - - const recapsPercentage = (mockUsage.dailyRecapsUsed / mockUsage.dailyRecapsLimit) * 100; - const tokensPercentage = (mockUsage.aiTokensUsed / mockUsage.aiTokensLimit) * 100; - const connectionsPercentage = (mockUsage.connectionsUsed / mockUsage.connectionsLimit) * 100; - - // Format plan name from API format (FREE, BASIC, PRO) to display format (Free, Basic, Pro) - const formatPlanName = (planType: string | null | undefined): string => { - if (!planType) return 'Free'; - return planType.charAt(0) + planType.slice(1).toLowerCase(); - }; - - const currentPlanName = formatPlanName(plan?.plan); - const isUpgrade = (planName: string) => { - const planOrder = ['Free', 'Basic', 'Pro']; - const currentIndex = planOrder.indexOf(currentPlanName); - const newIndex = planOrder.indexOf(planName); - return newIndex > currentIndex; - }; - - // Format date for display - const formatDate = (dateString: string | null | undefined): string => { - if (!dateString) return 'N/A'; - try { - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); - } catch { - return dateString; - } - }; - - // Get next billing date from subscription - const getNextBillingDate = (): string => { - if (plan?.subscription?.currentPeriodEnd) { - return formatDate(plan.subscription.currentPeriodEnd); - } - if (plan?.planExpiry) { - return formatDate(plan.planExpiry); - } - return 'N/A'; - }; - - const handleSwitchPlan = (planName: string) => { - setSelectedPlan(planName); - setShowSwitchModal(true); - }; - - const handleStripePayment = async () => { - if (!selectedPlan || selectedPlan === 'Free') return; - - const planType = selectedPlan.toLowerCase() as 'basic' | 'pro'; - try { - await handleStripeCheckout(planType, billingCycle); - setShowSwitchModal(false); - setSelectedPlan(null); - } catch { - // Error is already handled in the hook - } - }; - - const handleCoinbasePayment = async () => { - if (!selectedPlan || selectedPlan === 'Free') return; - - const planType = selectedPlan.toLowerCase() as 'basic' | 'pro'; - try { - await handleCoinbaseCheckout(planType); - setShowSwitchModal(false); - setSelectedPlan(null); - } catch { - // Error is already handled in the hook - } - }; - - // Get plan data from Stripe plans - const getPlanData = (planName: string) => { - return stripePlans.find((p) => p.name === planName); - }; - - return ( -
- -
-
-

- Dashboard -

-

Manage your subscription and usage

- - {/* Subscription Card */} -
- {loading ? ( -
-

Loading subscription information...

-
- ) : error ? ( -
-

{error}

- -
- ) : plan ? ( -
-
-

- Current Plan: {currentPlanName} -

-

- Status: {plan.hasActiveSubscription ? ( - Active - ) : ( - Inactive - )} -

- {plan.planExpiry && ( -

- Plan expires: {formatDate(plan.planExpiry)} -

- )} - {plan.subscription?.currentPeriodEnd && ( -

- Next billing date: {formatDate(plan.subscription.currentPeriodEnd)} -

- )} - {plan.subscription?.status && ( -

- Subscription status: {plan.subscription.status} -

- )} -
-
- {plan.hasActiveSubscription && ( - <> - - - - )} -
-
- ) : ( -
-

No subscription information available

-
- )} -
- - {/* Switch Plan Section */} -
-
-
-

Switch Plan

-

- Upgrade or downgrade your subscription plan -

-
- {/* Billing Cycle Toggle */} -
- - -
-
- -
- {plansLoading ? ( -
Loading plans...
- ) : ( - stripePlans.map((planData) => { - const isCurrentPlan = planData.name === currentPlanName; - const isUpgradePlan = isUpgrade(planData.name); - const price = billingCycle === 'monthly' ? planData.monthlyPrice : planData.annualPrice; - const monthlyEquivalent = billingCycle === 'annual' && price > 0 - ? Math.round(price / 12) - : price; - const savings = billingCycle === 'annual' && planData.monthlyPrice > 0 - ? (planData.monthlyPrice * 12) - price - : 0; - - return ( -
- {isCurrentPlan && ( -
- - Current - -
- )} -
-

{planData.name}

-
- - ${price} - - {price > 0 && ( - - /{billingCycle === 'monthly' ? 'month' : 'year'} - - )} -
- {billingCycle === 'annual' && price > 0 && ( -
-

- ${monthlyEquivalent}/month -

- {savings > 0 && ( -

- Save ${savings}/year -

- )} -
- )} -
- -
- ); - }) - )} -
-
- - {/* Usage Stats */} -
-

Usage Statistics

-
-
-
-
-

Daily Recaps Used

-

- {mockUsage.dailyRecapsUsed} -

-

- of {mockUsage.dailyRecapsLimit} -

-
-
-
-
-
-
- -
-
-
-

AI Tokens Used

-

- {mockUsage.aiTokensUsed.toLocaleString()} -

-

- of {mockUsage.aiTokensLimit.toLocaleString()} -

-
-
-
-
-
-
- -
-
-
-

Connections Used

-

- {mockUsage.connectionsUsed} -

-

- of {mockUsage.connectionsLimit} -

-
-
-
-
-
-
-
-
-
-
- - {/* Cancel Modal */} - {showCancelModal && ( -
-
- -

- Cancel Subscription -

-

- Are you sure you want to cancel your subscription? You'll lose - access to all premium features at the end of your billing period. -

-
- - -
-
-
- )} - - {/* Convert to Annual Modal */} - {showConvertModal && ( -
-
- -

- Convert to Annual Plan -

-

- Save 20% by switching to an annual plan. Your subscription will - be billed annually at $278.40 (equivalent to $23.20/month). -

-
- - -
-
-
- )} - - {/* Switch Plan Modal */} - {showSwitchModal && selectedPlan && ( -
-
- {/* Close Button */} - - -

- Switch to {selectedPlan} Plan -

-

- {isUpgrade(selectedPlan) ? ( - <> - You're upgrading from {currentPlanName} to{' '} - {selectedPlan}. Changes will take effect - immediately. You'll be charged a prorated amount for the - remainder of your billing cycle. - - ) : ( - <> - You're downgrading from {currentPlanName} to{' '} - {selectedPlan}. Changes will take effect at - the end of your current billing period on{' '} - {getNextBillingDate()}. - - )} -

- - {selectedPlan !== 'Free' && ( - <> - {/* Price Display */} -
- {(() => { - const planData = getPlanData(selectedPlan); - if (!planData) return null; - - const price = billingCycle === 'monthly' - ? planData.monthlyPrice - : planData.annualPrice; - const monthlyEquivalent = billingCycle === 'annual' && price > 0 - ? Math.round(price / 12) - : price; - - return ( - <> -

- {billingCycle === 'monthly' ? 'Monthly' : 'Annual'} charge: ${price} - {billingCycle === 'annual' && price > 0 && ( - - (${monthlyEquivalent}/month) - - )} -

- - ); - })()} -
- - {/* Payment Buttons */} -
- - -
- - )} -
-
- )} -
- ); -} diff --git a/lander/app/feedback/page.tsx b/lander/app/feedback/page.tsx deleted file mode 100644 index 4a9d55bfd..000000000 --- a/lander/app/feedback/page.tsx +++ /dev/null @@ -1,702 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import TelegramLogin from '../components/TelegramLogin'; -import FeedbackList from '../components/FeedbackList'; - -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; -const USE_MOCK_DATA = true; // Set to false when ready to use real API - -export interface Feedback { - _id: string; - title: string; - description?: string; - status: 'planned' | 'in_progress' | 'complete'; - type: 'feature' | 'bug' | 'improvement'; - createdBy: { - telegramId: number; - telegramFirstName?: string; - telegramLastName?: string; - telegramUsername?: string; - }; - upvotes: number; - downvotes: number; - userVote: 'upvote' | 'downvote' | null; - comments: string[]; - createdAt: string; - updatedAt: string; -} - -// Mock data -const mockFeedbacks: Feedback[] = [ - { - _id: '1', - title: 'Summarize conversations', - description: 'Get up to speed more quickly in any channel, direct message (DM), or thread. Whether we\'re joining a new channel, returning from time off, or catching up on missed messages.', - status: 'planned', - type: 'feature', - createdBy: { - telegramId: 123456789, - telegramFirstName: 'John', - telegramLastName: 'Doe', - telegramUsername: 'johndoe', - }, - upvotes: 5, - downvotes: 0, - userVote: null, - comments: ['1', '2'], - createdAt: new Date(Date.now() - 86400000).toISOString(), - updatedAt: new Date(Date.now() - 86400000).toISOString(), - }, - { - _id: '2', - title: 'Create a daily recap of selected chats', - description: 'Get caught up after some time away from Telegram with a customized summary of our unread channels. With recaps, we can get automated daily summaries for selected chats.', - status: 'planned', - type: 'feature', - createdBy: { - telegramId: 987654321, - telegramFirstName: 'Jane', - telegramLastName: 'Smith', - telegramUsername: 'janesmith', - }, - upvotes: 3, - downvotes: 1, - userVote: null, - comments: ['3'], - createdAt: new Date(Date.now() - 172800000).toISOString(), - updatedAt: new Date(Date.now() - 172800000).toISOString(), - }, - { - _id: '3', - title: 'Create a macOS & Windows App', - description: 'For all the desktop users using these platforms', - status: 'planned', - type: 'feature', - createdBy: { - telegramId: 555555555, - telegramFirstName: 'Bob', - telegramLastName: 'Johnson', - telegramUsername: 'bobjohnson', - }, - upvotes: 8, - downvotes: 0, - userVote: null, - comments: [], - createdAt: new Date(Date.now() - 259200000).toISOString(), - updatedAt: new Date(Date.now() - 259200000).toISOString(), - }, - { - _id: '4', - title: 'Fix message sync issues', - description: 'Messages sometimes don\'t sync properly across devices', - status: 'in_progress', - type: 'bug', - createdBy: { - telegramId: 111222333, - telegramFirstName: 'Alice', - telegramLastName: 'Williams', - telegramUsername: 'alicew', - }, - upvotes: 12, - downvotes: 0, - userVote: null, - comments: ['4', '5'], - createdAt: new Date(Date.now() - 345600000).toISOString(), - updatedAt: new Date(Date.now() - 345600000).toISOString(), - }, - { - _id: '5', - title: 'Dark mode improvements', - description: 'Better contrast and color scheme for dark mode', - status: 'complete', - type: 'improvement', - createdBy: { - telegramId: 444555666, - telegramFirstName: 'Charlie', - telegramLastName: 'Brown', - telegramUsername: 'charlieb', - }, - upvotes: 7, - downvotes: 0, - userVote: null, - comments: ['6'], - createdAt: new Date(Date.now() - 432000000).toISOString(), - updatedAt: new Date(Date.now() - 432000000).toISOString(), - }, -]; - -const mockComments: Record> = { - '1': [ - { - _id: '1', - content: 'This would be really helpful for catching up on group chats!', - user: { - telegramId: 999888777, - telegramFirstName: 'David', - telegramLastName: 'Lee', - telegramUsername: 'davidlee', - }, - createdAt: new Date(Date.now() - 82800000).toISOString(), - }, - ], - '2': [ - { - _id: '2', - content: 'Great idea! Would love to see this implemented.', - user: { - telegramId: 123456789, - telegramFirstName: 'John', - telegramLastName: 'Doe', - telegramUsername: 'johndoe', - }, - createdAt: new Date(Date.now() - 82000000).toISOString(), - }, - ], - '3': [ - { - _id: '3', - content: 'This feature would save me so much time!', - user: { - telegramId: 777666555, - telegramFirstName: 'Emma', - telegramLastName: 'Davis', - telegramUsername: 'emmad', - }, - createdAt: new Date(Date.now() - 170000000).toISOString(), - }, - ], - '4': [ - { - _id: '4', - content: 'I\'ve experienced this issue multiple times. Hope it gets fixed soon!', - user: { - telegramId: 333444555, - telegramFirstName: 'Frank', - telegramLastName: 'Miller', - telegramUsername: 'frankm', - }, - createdAt: new Date(Date.now() - 340000000).toISOString(), - }, - { - _id: '5', - content: 'Same here, very annoying bug.', - user: { - telegramId: 666777888, - telegramFirstName: 'Grace', - telegramLastName: 'Wilson', - telegramUsername: 'gracew', - }, - createdAt: new Date(Date.now() - 338000000).toISOString(), - }, - ], - '6': [ - { - _id: '6', - content: 'The new dark mode looks amazing! Great work!', - user: { - telegramId: 111222333, - telegramFirstName: 'Alice', - telegramLastName: 'Williams', - telegramUsername: 'alicew', - }, - createdAt: new Date(Date.now() - 428000000).toISOString(), - }, - ], -}; - -// Global mock state (in a real app, this would be in a state management solution) -let mockFeedbacksState = [...mockFeedbacks]; -let mockToken: string | null = null; - -// Export functions to update mock state -export const updateMockFeedback = (feedbackId: string, updates: Partial) => { - const index = mockFeedbacksState.findIndex(f => f._id === feedbackId); - if (index !== -1) { - mockFeedbacksState[index] = { ...mockFeedbacksState[index], ...updates }; - } -}; - -export const getMockFeedbacks = () => mockFeedbacksState; -export const setMockFeedbacks = (feedbacks: Feedback[]) => { - mockFeedbacksState = feedbacks; -}; - -export default function FeedbackPage() { - const [feedbacks, setFeedbacks] = useState([]); - const [loading, setLoading] = useState(true); - const [token, setToken] = useState(null); - const [user, setUser] = useState<{ id: number; first_name?: string; last_name?: string; username?: string } | null>(null); - const [showCreateForm, setShowCreateForm] = useState(false); - const [newFeedback, setNewFeedback] = useState<{ title: string; description: string; type: 'feature' | 'bug' | 'improvement' }>({ title: '', description: '', type: 'feature' }); - const [selectedBoard, setSelectedBoard] = useState<'feature' | 'bug'>('feature'); - const [sortBy, setSortBy] = useState<'trending' | 'newest' | 'oldest'>('trending'); - const [searchQuery, setSearchQuery] = useState(''); - const [sidebarOpen, setSidebarOpen] = useState(false); - - useEffect(() => { - // Check for stored token - const storedToken = localStorage.getItem('telegram_token'); - if (storedToken) { - setToken(storedToken); - mockToken = storedToken; - // Mock user data - setUser({ - id: 123456789, - first_name: 'John', - last_name: 'Doe', - username: 'johndoe', - }); - } - loadFeedbacks(); - }, []); - - const loadFeedbacks = async () => { - try { - if (USE_MOCK_DATA) { - // Mock API response - await new Promise(resolve => setTimeout(resolve, 300)); // Simulate network delay - - // Sync mock state - initialize if empty - if (mockFeedbacksState.length === 0) { - mockFeedbacksState = [...mockFeedbacks]; - } - - // Use current mockFeedbacksState - let filteredFeedbacks = [...mockFeedbacksState].filter(f => { - if (selectedBoard === 'bug') { - return f.type === 'bug'; - } - return f.type === 'feature'; - }); - - // Sort feedbacks - if (sortBy === 'trending') { - filteredFeedbacks.sort((a, b) => (b.upvotes - b.downvotes) - (a.upvotes - a.downvotes)); - } else if (sortBy === 'newest') { - filteredFeedbacks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - } else { - filteredFeedbacks.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - } - - // Filter by search query - if (searchQuery) { - filteredFeedbacks = filteredFeedbacks.filter( - (f) => - f.title.toLowerCase().includes(searchQuery.toLowerCase()) || - f.description?.toLowerCase().includes(searchQuery.toLowerCase()) - ); - } - - setFeedbacks(filteredFeedbacks); - } else { - const storedToken = localStorage.getItem('telegram_token'); - const headers: HeadersInit = {}; - if (storedToken) { - headers['Authorization'] = `Bearer ${storedToken}`; - } - - const params = new URLSearchParams(); - if (selectedBoard === 'bug') { - params.append('type', 'bug'); - } else { - params.append('type', 'feature'); - } - - const response = await fetch(`${API_BASE_URL}/api/feedback?${params.toString()}`, { headers }); - const data = await response.json(); - if (data.success) { - let sortedFeedbacks = [...data.data]; - - // Sort feedbacks - if (sortBy === 'trending') { - sortedFeedbacks.sort((a, b) => (b.upvotes - b.downvotes) - (a.upvotes - a.downvotes)); - } else if (sortBy === 'newest') { - sortedFeedbacks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - } else { - sortedFeedbacks.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - } - - // Filter by search query - if (searchQuery) { - sortedFeedbacks = sortedFeedbacks.filter( - (f) => - f.title.toLowerCase().includes(searchQuery.toLowerCase()) || - f.description?.toLowerCase().includes(searchQuery.toLowerCase()) - ); - } - - setFeedbacks(sortedFeedbacks); - } - } - } catch (error) { - console.error('Failed to load feedbacks:', error); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadFeedbacks(); - }, [selectedBoard, sortBy, searchQuery]); - - const handleTelegramAuth = async (telegramUser: { id: number; first_name?: string; last_name?: string; username?: string; auth_date: number; hash: string }) => { - try { - if (USE_MOCK_DATA) { - // Mock login response - await new Promise(resolve => setTimeout(resolve, 300)); - const mockJwtToken = 'mock_jwt_token_' + Date.now(); - localStorage.setItem('telegram_token', mockJwtToken); - setToken(mockJwtToken); - mockToken = mockJwtToken; - setUser(telegramUser); - await loadFeedbacks(); - } else { - const response = await fetch(`${API_BASE_URL}/telegram/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - telegramId: telegramUser.id, - telegramUser: { - id: telegramUser.id, - first_name: telegramUser.first_name, - last_name: telegramUser.last_name, - username: telegramUser.username, - }, - }), - }); - - const data = await response.json(); - if (data.success && data.token) { - localStorage.setItem('telegram_token', data.token); - setToken(data.token); - setUser(telegramUser); - await loadFeedbacks(); - } - } - } catch (error) { - console.error('Failed to login:', error); - } - }; - - const handleLogout = () => { - localStorage.removeItem('telegram_token'); - setToken(null); - mockToken = null; - setUser(null); - }; - - const handleCreateFeedback = async () => { - if (!token || !newFeedback.title.trim()) return; - - try { - if (USE_MOCK_DATA) { - // Mock create feedback - await new Promise(resolve => setTimeout(resolve, 300)); - const newId = String(Date.now()); - const newFeedbackItem: Feedback = { - _id: newId, - title: newFeedback.title, - description: newFeedback.description, - status: 'planned', - type: newFeedback.type, - createdBy: { - telegramId: user?.id || 123456789, - telegramFirstName: user?.first_name, - telegramLastName: user?.last_name, - telegramUsername: user?.username, - }, - upvotes: 0, - downvotes: 0, - userVote: null, - comments: [], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - mockFeedbacksState.push(newFeedbackItem); - setMockFeedbacks(mockFeedbacksState); - setShowCreateForm(false); - setNewFeedback({ title: '', description: '', type: selectedBoard === 'bug' ? ('bug' as const) : ('feature' as const) }); - await loadFeedbacks(); - } else { - const response = await fetch(`${API_BASE_URL}/api/feedback`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify(newFeedback), - }); - - const data = await response.json(); - if (data.success) { - setShowCreateForm(false); - setNewFeedback({ title: '', description: '', type: selectedBoard === 'bug' ? ('bug' as const) : ('feature' as const) }); - await loadFeedbacks(); - } - } - } catch (error) { - console.error('Failed to create feedback:', error); - } - }; - - if (loading) { - return ( -
-
-
Loading...
-
-
- ); - } - - const boardTitle = selectedBoard === 'bug' ? 'Bugs' : 'Feature Requests'; - const boardDescription = selectedBoard === 'bug' - ? "Report any bugs or issues you've encountered. We'll prioritize fixing them based on upvotes." - : "Any feature that you'd like to request the team to build can go in over here. We prioritize requests based on upvotes."; - - return ( -
- {/* Custom Header - Mobile Responsive */} -
-
-
-
- -

OpenHuman

-
- - -
-
-
- {token ? ( - <> - {user && ( -
- {user.first_name} {user.last_name || ''} {user.username && `(@${user.username})`} -
- )} - - - ) : ( - - )} -
-
-
-
- -
-
- {/* Mobile Sidebar Overlay */} - {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - - {/* Sidebar - Mobile Drawer / Desktop Sidebar */} - - - {/* Main Content */} -
-
-

{boardTitle}

-

{boardDescription}

-
- - {/* Create Form */} - {showCreateForm ? ( -
-
-
- setNewFeedback({ ...newFeedback, title: e.target.value })} - className="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-4 py-3 text-sm sm:text-base text-white placeholder-zinc-500 focus:border-zinc-700 focus:outline-none" - placeholder="Short, descriptive title" - /> -
-
- -