Merge pull request #81 from vernu/web-dashboard-ui

UI/UX redesign of web dashboard
This commit is contained in:
Israel Abebe
2025-06-04 20:47:12 +03:00
committed by GitHub
74 changed files with 3369 additions and 2897 deletions

View File

@@ -62,7 +62,6 @@ jobs:
run: |
cd web
pnpm install
pnpm run prisma:generate
pnpm run build
build-and-test-android:

View File

@@ -88,10 +88,10 @@ export default function LoginForm() {
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type='password'
{...field}
className='dark:text-white dark:bg-gray-800'
<Input
type='password'
{...field}
className='dark:text-white dark:bg-gray-800'
/>
</FormControl>
<FormMessage />

View File

@@ -123,7 +123,7 @@ export default function RequestPasswordResetForm() {
<CardFooter className='text-center'>
<Link
href={Routes.login}
className='text-sm text-blue-600 hover:underline'
className='text-sm text-brand-600 hover:underline'
>
Back to login
</Link>

View File

@@ -188,7 +188,7 @@ export default function ResetPasswordForm({
<CardFooter className='text-center'>
<Link
href={Routes.login}
className='text-sm text-blue-600 hover:underline'
className='text-sm text-brand-600 hover:underline'
>
Back to login
</Link>

View File

@@ -50,7 +50,7 @@ export default function LoginPage() {
<CardFooter className='flex flex-col space-y-2 text-center'>
<Link
href={Routes.resetPassword}
className='text-sm text-blue-600 hover:underline'
className='text-sm text-brand-600 hover:underline'
>
Forgot your password?
</Link>
@@ -63,7 +63,7 @@ export default function LoginPage() {
redirect: redirect ? decodeURIComponent(redirect) : undefined,
},
}}
className='font-medium text-blue-600 hover:underline'
className='font-medium text-brand-600 hover:underline'
>
Sign up
</Link>

View File

@@ -56,7 +56,7 @@ export default function RegisterPage() {
redirect: redirect ? decodeURIComponent(redirect) : undefined,
},
}}
className='font-medium text-blue-600 hover:underline'
className='font-medium text-brand-600 hover:underline'
>
Sign in
</Link>

View File

@@ -40,8 +40,8 @@ const SuccessAlert = ({ title, message }: { title: string; message: string }) =>
)
const InfoAlert = ({ title, message }: { title: string; message: string }) => (
<Alert className='bg-blue-50 text-blue-700 border-blue-200'>
<Mail className='h-5 w-5 text-blue-600' />
<Alert className='bg-brand-50 text-brand-700 border-brand-200'>
<Mail className='h-5 w-5 text-brand-600' />
<AlertTitle className='text-lg font-semibold'>{title}</AlertTitle>
<AlertDescription>{message}</AlertDescription>
</Alert>
@@ -54,7 +54,7 @@ const LoadingSpinner = () => (
)
const DashboardButton = () => (
<Button className='w-full py-5 mt-2' asChild>
<Button className='w-full py-5 mt-2 text-white' asChild>
<Link href={Routes.dashboard}>
Go to Dashboard
<ArrowRight className='ml-2 h-5 w-5' />

View File

@@ -13,26 +13,29 @@ export default function CheckoutPage({ params }) {
const { data: session } = useSession()
const initiateCheckout = useCallback(async (retries = 2) => {
try {
const response = await httpBrowserClient.post('/billing/checkout', {
planName,
})
const initiateCheckout = useCallback(
async (retries = 2) => {
try {
const response = await httpBrowserClient.post('/billing/checkout', {
planName,
})
if (response.data?.redirectUrl) {
window.location.href = response.data?.redirectUrl
} else {
throw new Error('No redirect URL found')
if (response.data?.redirectUrl) {
window.location.href = response.data?.redirectUrl
} else {
throw new Error('No redirect URL found')
}
} catch (error) {
if (retries > 0) {
initiateCheckout(retries - 1)
} else {
setError('Failed to create checkout session. Please try again.')
console.error(error)
}
}
} catch (error) {
if (retries > 0) {
initiateCheckout(retries - 1)
} else {
setError('Failed to create checkout session. Please try again.')
console.error(error)
}
}
}, [planName])
},
[planName]
)
useEffect(() => {
initiateCheckout()
@@ -51,7 +54,7 @@ export default function CheckoutPage({ params }) {
setError(null)
initiateCheckout()
}}
className='mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600'
className='mt-4 px-4 py-2 bg-brand-500 text-white rounded hover:bg-brand-600'
>
Try Again
</button>
@@ -61,7 +64,7 @@ export default function CheckoutPage({ params }) {
return (
<div className='flex flex-col items-center justify-center min-h-[80vh] bg-gray-100 p-6 rounded-lg shadow-lg'>
<Loader className='animate-spin mb-4 text-blue-500' size={48} />
<Loader className='animate-spin mb-4 text-brand-500' size={48} />
<h2 className='text-2xl font-bold text-gray-800 mb-2'>Hang Tight!</h2>
<p className='text-lg text-gray-600 mb-4'>
We're processing your order. This won't take long!

View File

@@ -218,7 +218,7 @@ export default function AccountSettings() {
if (isLoadingSubscription)
return (
<div className='flex justify-center items-center h-full'>
<div className='flex justify-center items-center h-full min-h-[200px] mt-10'>
<Spinner size='sm' />
</div>
)
@@ -230,22 +230,25 @@ export default function AccountSettings() {
)
// Format price with currency symbol
const formatPrice = (amount: number | null | undefined, currency: string | null | undefined) => {
if (amount == null || currency == null) return 'Free';
const formatPrice = (
amount: number | null | undefined,
currency: string | null | undefined
) => {
if (amount == null || currency == null) return 'Free'
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase() || 'USD',
minimumFractionDigits: 2,
});
return formatter.format(amount / 100);
};
})
return formatter.format(amount / 100)
}
const getBillingInterval = (interval: string | null | undefined) => {
if (!interval) return '';
return interval.toLowerCase() === 'month' ? 'monthly' : 'yearly';
};
if (!interval) return ''
return interval.toLowerCase() === 'month' ? 'monthly' : 'yearly'
}
return (
<div className='bg-gradient-to-br from-white to-gray-50 dark:from-gray-800 dark:to-gray-900 border rounded-lg shadow p-4'>
@@ -259,40 +262,54 @@ export default function AccountSettings() {
Current subscription
</p>
{currentSubscription?.amount > 0 && (
<Badge variant="outline" className="text-xs font-medium">
{formatPrice(currentSubscription?.amount, currentSubscription?.currency)}
<Badge variant='outline' className='text-xs font-medium'>
{formatPrice(
currentSubscription?.amount,
currentSubscription?.currency
)}
{currentSubscription?.recurringInterval && (
<span className="ml-1">/ {getBillingInterval(currentSubscription?.recurringInterval)}</span>
<span className='ml-1'>
/{' '}
{getBillingInterval(
currentSubscription?.recurringInterval
)}
</span>
)}
</Badge>
)}
</div>
</div>
<div className={`flex items-center px-2 py-0.5 rounded-full ${
currentSubscription?.status === 'active'
? 'bg-green-50 dark:bg-green-900/30'
: currentSubscription?.status === 'past_due'
<div
className={`flex items-center px-2 py-0.5 rounded-full ${
currentSubscription?.status === 'active'
? 'bg-green-50 dark:bg-green-900/30'
: currentSubscription?.status === 'past_due'
? 'bg-amber-50 dark:bg-amber-900/30'
: 'bg-gray-50 dark:bg-gray-800/50'
}`}>
<Check className={`h-3 w-3 mr-1 ${
currentSubscription?.status === 'active'
? 'text-green-600 dark:text-green-400'
: currentSubscription?.status === 'past_due'
}`}
>
<Check
className={`h-3 w-3 mr-1 ${
currentSubscription?.status === 'active'
? 'text-green-600 dark:text-green-400'
: currentSubscription?.status === 'past_due'
? 'text-amber-600 dark:text-amber-400'
: 'text-gray-600 dark:text-gray-400'
}`} />
<span className={`text-xs font-medium ${
currentSubscription?.status === 'active'
? 'text-green-600 dark:text-green-400'
: currentSubscription?.status === 'past_due'
}`}
/>
<span
className={`text-xs font-medium ${
currentSubscription?.status === 'active'
? 'text-green-600 dark:text-green-400'
: currentSubscription?.status === 'past_due'
? 'text-amber-600 dark:text-amber-400'
: 'text-gray-600 dark:text-gray-400'
}`}>
{currentSubscription?.status
}`}
>
{currentSubscription?.status
? currentSubscription.status
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
: 'Active'}
</span>
@@ -301,7 +318,7 @@ export default function AccountSettings() {
<div className='grid grid-cols-2 gap-3'>
<div className='flex items-center space-x-2 bg-white dark:bg-gray-800 p-2 rounded-md shadow-sm'>
<Calendar className='h-4 w-4 text-blue-600 dark:text-blue-400' />
<Calendar className='h-4 w-4 text-brand-600 dark:text-brand-400' />
<div>
<p className='text-xs text-gray-500 dark:text-gray-400'>
Start Date
@@ -321,7 +338,7 @@ export default function AccountSettings() {
</div>
<div className='flex items-center space-x-2 bg-white dark:bg-gray-800 p-2 rounded-md shadow-sm'>
<Calendar className='h-4 w-4 text-blue-600 dark:text-blue-400' />
<Calendar className='h-4 w-4 text-brand-600 dark:text-brand-400' />
<div>
<p className='text-xs text-gray-500 dark:text-gray-400'>
Next Payment
@@ -376,7 +393,8 @@ export default function AccountSettings() {
<p className='text-sm font-medium text-gray-900 dark:text-white'>
{currentSubscription?.plan?.monthlyLimit === -1
? 'Unlimited'
: currentSubscription?.plan?.monthlyLimit?.toLocaleString() || '0'}
: currentSubscription?.plan?.monthlyLimit?.toLocaleString() ||
'0'}
{currentSubscription?.plan?.monthlyLimit === -1 && (
<TooltipProvider>
<Tooltip>
@@ -420,10 +438,11 @@ export default function AccountSettings() {
</div>
<div className='mt-4 flex justify-end gap-2'>
{(!currentSubscription?.plan?.name || currentSubscription?.plan?.name?.toLowerCase() === 'free') ? (
{!currentSubscription?.plan?.name ||
currentSubscription?.plan?.name?.toLowerCase() === 'free' ? (
<Link
href='/checkout/pro'
className='text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 px-3 py-1.5 rounded-md transition-colors'
className='text-xs font-medium text-white bg-brand-600 hover:bg-brand-700 px-3 py-1.5 rounded-md transition-colors'
>
Upgrade to Pro
</Link>
@@ -442,13 +461,13 @@ export default function AccountSettings() {
if (isLoadingUser)
return (
<div className='flex justify-center items-center h-full'>
<div className='flex justify-center items-center h-full min-h-[200px] mt-10'>
<Spinner size='sm' />
</div>
)
return (
<div className='grid gap-6 max-w-2xl mx-auto'>
<div className='grid gap-6 max-w-2xl mt-10'>
<CurrentSubscription />
<Card>
<CardHeader>
@@ -599,7 +618,7 @@ export default function AccountSettings() {
</div>
<div className='space-y-2'>
<Label htmlFor='oldPassword'>Old Password</Label>
<Label htmlFor='newPassword'>New Password</Label>
<Input
id='newPassword'
type='password'
@@ -714,9 +733,10 @@ export default function AccountSettings() {
{requestAccountDeletionError && (
<p className='text-sm text-destructive'>
{(requestAccountDeletionError as any).response?.data?.message ||
requestAccountDeletionError.message ||
'Failed to submit account deletion request'}
{(requestAccountDeletionError as any).response?.data
?.message ||
requestAccountDeletionError.message ||
'Failed to submit account deletion request'}
</p>
)}

View File

@@ -129,7 +129,6 @@ export default function ApiKeys() {
<CardTitle className='text-lg'>API Keys</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className='h-[400px] pr-4'>
<div className='space-y-2'>
{isPending && (
<>
@@ -255,7 +254,6 @@ export default function ApiKeys() {
</Card>
))}
</div>
</ScrollArea>
{/* Revoke Dialog */}
<Dialog open={isRevokeDialogOpen} onOpenChange={setIsRevokeDialogOpen}>

View File

@@ -143,7 +143,7 @@ export default function BulkSMSSend() {
const isStep3Disabled = isStep2Disabled || !selectedColumn || !messageTemplate
return (
<div className='container mx-auto p-4 space-y-8'>
<div className='space-y-8'>
<Card>
<CardHeader>
<CardTitle>Send Bulk SMS</CardTitle>

View File

@@ -0,0 +1,155 @@
'use client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Loader2 } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { useToast } from '@/hooks/use-toast'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { useMutation } from '@tanstack/react-query'
import Link from 'next/link'
import { Routes } from '@/config/routes'
const changePasswordSchema = z
.object({
oldPassword: z.string().min(1, 'Old password is required'),
newPassword: z
.string()
.min(8, { message: 'Password must be at least 8 characters long' }),
confirmPassword: z
.string()
.min(4, { message: 'Please confirm your password' }),
})
.superRefine((data, ctx) => {
if (data.newPassword !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Passwords must match',
path: ['confirmPassword'],
})
}
})
type ChangePasswordFormData = z.infer<typeof changePasswordSchema>
export default function ChangePasswordForm() {
const { toast } = useToast()
const changePasswordForm = useForm<ChangePasswordFormData>({
resolver: zodResolver(changePasswordSchema),
})
const {
mutate: changePassword,
isPending: isChangingPassword,
error: changePasswordError,
isSuccess: isChangePasswordSuccess,
} = useMutation({
mutationFn: (data: ChangePasswordFormData) =>
httpBrowserClient.post(ApiEndpoints.auth.changePassword(), data),
onSuccess: () => {
toast({
title: 'Password changed successfully!',
})
changePasswordForm.reset()
},
onError: (error) => {
const errorMessage = (error as any).response?.data?.error
changePasswordForm.setError('root.serverError', {
message: errorMessage || 'Failed to change password',
})
toast({
title: 'Failed to change password',
})
},
})
return (
<>
<p className='text-sm text-muted-foreground mb-4'>
If you signed in with Google, you can reset your password{' '}
<Link href={Routes.resetPassword} className='underline'>
here
</Link>
.
</p>
<form
onSubmit={changePasswordForm.handleSubmit((data) => changePassword(data))}
className='space-y-4'
>
<div className='space-y-2'>
<Label htmlFor='oldPassword'>Old Password</Label>
<Input
id='oldPassword'
type='password'
{...changePasswordForm.register('oldPassword')}
placeholder='Enter your old password'
/>
{changePasswordForm.formState.errors.oldPassword && (
<p className='text-sm text-destructive'>
{changePasswordForm.formState.errors.oldPassword.message}
</p>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='newPassword'>New Password</Label>
<Input
id='newPassword'
type='password'
{...changePasswordForm.register('newPassword')}
placeholder='Enter your new password'
/>
{changePasswordForm.formState.errors.newPassword && (
<p className='text-sm text-destructive'>
{changePasswordForm.formState.errors.newPassword.message}
</p>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='confirmPassword'>Confirm Password</Label>
<Input
id='confirmPassword'
type='password'
{...changePasswordForm.register('confirmPassword')}
placeholder='Enter your confirm password'
/>
{changePasswordForm.formState.errors.confirmPassword && (
<p className='text-sm text-destructive'>
{changePasswordForm.formState.errors.confirmPassword.message}
</p>
)}
</div>
{changePasswordForm.formState.errors.root?.serverError && (
<p className='text-sm text-destructive'>
{changePasswordForm.formState.errors.root.serverError.message}
</p>
)}
{isChangePasswordSuccess && (
<p className='text-sm text-green-500'>
Password changed successfully!
</p>
)}
<Button
type='submit'
className='w-full mt-6'
disabled={isChangingPassword}
>
{isChangingPassword ? (
<Loader2 className='h-4 w-4 animate-spin mr-2' />
) : null}
Change Password
</Button>
</form>
</>
)
}

View File

@@ -6,8 +6,8 @@ import { ExternalLinks } from '@/config/external-links'
export default function CommunityLinks() {
return (
<div className='grid gap-4 md:grid-cols-4'>
<Card>
<div className='grid gap-4 md:grid-cols-2'>
{/* <Card>
<CardHeader>
<CardTitle>One-time Donation</CardTitle>
</CardHeader>
@@ -22,9 +22,9 @@ export default function CommunityLinks() {
</Button>
</Link>
</CardContent>
</Card>
</Card> */}
<Card>
{/* <Card>
<CardHeader>
<CardTitle>Support on Patreon</CardTitle>
</CardHeader>
@@ -39,7 +39,7 @@ export default function CommunityLinks() {
</Button>
</Link>
</CardContent>
</Card>
</Card> */}
<Card>
<CardHeader>

View File

@@ -0,0 +1,160 @@
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { AlertTriangle, Loader2 } from 'lucide-react'
import { useToast } from '@/hooks/use-toast'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { useMutation, useQuery } from '@tanstack/react-query'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
export default function DangerZoneForm() {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
const [deleteConfirmEmail, setDeleteConfirmEmail] = useState('')
const [deleteReason, setDeleteReason] = useState('')
const { toast } = useToast()
const { data: currentUser } = useQuery({
queryKey: ['currentUser'],
queryFn: () =>
httpBrowserClient
.get(ApiEndpoints.auth.whoAmI())
.then((res) => res.data?.data),
})
const handleDeleteAccount = () => {
if (deleteConfirmEmail !== currentUser?.email) {
toast({
title: 'Please enter your correct email address',
})
return
}
requestAccountDeletion()
}
const {
mutate: requestAccountDeletion,
isPending: isRequestingAccountDeletion,
error: requestAccountDeletionError,
isSuccess: isRequestAccountDeletionSuccess,
} = useMutation({
mutationFn: () =>
httpBrowserClient.post(ApiEndpoints.support.requestAccountDeletion(), {
message: deleteReason,
}),
onSuccess: () => {
toast({
title: 'Account deletion request submitted',
})
setIsDeleteDialogOpen(false)
},
onError: () => {
toast({
title: 'Failed to submit account deletion request',
})
},
})
return (
<>
<p className='text-sm text-muted-foreground mb-6'>
Once you delete your account, there is no going back. This action
permanently removes all your data, cancels subscriptions, and revokes
access to all services.
</p>
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<Button
variant='destructive'
className='w-full'
onClick={() => setIsDeleteDialogOpen(true)}
>
<AlertTriangle className='mr-2 h-4 w-4' />
Delete Account
</Button>
<DialogContent>
<DialogHeader>
<DialogTitle className='flex items-center gap-2'>
<AlertTriangle className='h-5 w-5 text-destructive' />
Delete Account
</DialogTitle>
<DialogDescription className='pt-4'>
<p className='mb-4'>
Are you sure you want to delete your account? This action:
</p>
<ul className='list-disc list-inside space-y-2 mb-4'>
<li>Cannot be undone</li>
<li>Will permanently delete all your data</li>
<li>Will cancel all active subscriptions</li>
<li>Will remove access to all services</li>
</ul>
<Label htmlFor='deleteReason'>Reason for deletion</Label>
<Textarea
className='my-2'
placeholder='Enter your reason for deletion'
value={deleteReason}
onChange={(e) => setDeleteReason(e.target.value)}
/>
<p>Please type your email address to confirm:</p>
<Input
className='mt-2'
placeholder='Enter your email address'
value={deleteConfirmEmail}
onChange={(e) => setDeleteConfirmEmail(e.target.value)}
/>
{requestAccountDeletionError && (
<p className='text-sm text-destructive'>
{(requestAccountDeletionError as any).response?.data
?.message ||
requestAccountDeletionError.message ||
'Failed to submit account deletion request'}
</p>
)}
{isRequestAccountDeletionSuccess && (
<p className='text-sm text-green-500'>
Account deletion request submitted
</p>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter className='gap-2 sm:gap-0'>
<Button
variant='outline'
onClick={() => setIsDeleteDialogOpen(false)}
>
Cancel
</Button>
<Button
variant='destructive'
onClick={handleDeleteAccount}
disabled={isRequestingAccountDeletion || !deleteConfirmEmail}
>
{isRequestingAccountDeletion ? (
<Loader2 className='h-4 w-4 animate-spin mr-2' />
) : (
<AlertTriangle className='h-4 w-4 mr-2' />
)}
Delete Account
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -39,7 +39,6 @@ export default function DeviceList() {
<CardTitle className='text-lg'>Registered Devices</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className='h-[400px] pr-4'>
<div className='space-y-2'>
{isPending && (
<>
@@ -129,7 +128,6 @@ export default function DeviceList() {
</Card>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
)

View File

@@ -0,0 +1,192 @@
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Shield, Loader2, Mail, Check, UserCircle } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { useToast } from '@/hooks/use-toast'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { useMutation, useQuery } from '@tanstack/react-query'
import { Spinner } from '@/components/ui/spinner'
import { useSession } from 'next-auth/react'
const updateProfileSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email().optional(),
phone: z
.string()
.regex(/^\+?\d{0,14}$/, 'Invalid phone number')
.optional(),
})
type UpdateProfileFormData = z.infer<typeof updateProfileSchema>
export default function EditProfileForm() {
const { toast } = useToast()
const { update: updateSession } = useSession()
const {
data: currentUser,
isLoading: isLoadingUser,
refetch: refetchCurrentUser,
} = useQuery({
queryKey: ['currentUser'],
queryFn: () =>
httpBrowserClient
.get(ApiEndpoints.auth.whoAmI())
.then((res) => res.data?.data),
})
const updateProfileForm = useForm<UpdateProfileFormData>({
resolver: zodResolver(updateProfileSchema),
defaultValues: {
name: currentUser?.name,
email: currentUser?.email,
phone: currentUser?.phone,
},
})
const handleVerifyEmail = () => {
// TODO: Implement email verification
}
const {
mutate: updateProfile,
isPending: isUpdatingProfile,
error: updateProfileError,
isSuccess: isUpdateProfileSuccess,
} = useMutation({
mutationFn: (data: UpdateProfileFormData) =>
httpBrowserClient.patch(ApiEndpoints.auth.updateProfile(), data),
onSuccess: () => {
refetchCurrentUser()
toast({
title: 'Profile updated successfully!',
})
updateSession({
name: updateProfileForm.getValues().name,
phone: updateProfileForm.getValues().phone,
})
},
onError: () => {
toast({
title: 'Failed to update profile',
})
},
})
if (isLoadingUser)
return (
<div className='flex justify-center items-center h-full min-h-[200px]'>
<Spinner size='sm' />
</div>
)
return (
<form
onSubmit={updateProfileForm.handleSubmit((data) => updateProfile(data))}
className='space-y-4'
>
<div className='space-y-2'>
<Label htmlFor='name'>Full Name</Label>
<Input
id='name'
{...updateProfileForm.register('name')}
placeholder='Enter your full name'
defaultValue={currentUser?.name}
/>
{updateProfileForm.formState.errors.name && (
<p className='text-sm text-destructive'>
{updateProfileForm.formState.errors.name.message}
</p>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='email' className='flex items-center gap-2'>
Email Address
{currentUser?.emailVerifiedAt && (
<Badge variant='secondary' className='ml-2'>
<Shield className='h-3 w-3 mr-1' />
Verified
</Badge>
)}
</Label>
<div className='flex gap-2'>
<Input
id='email'
type='email'
{...updateProfileForm.register('email')}
placeholder='Enter your email'
defaultValue={currentUser?.email}
disabled
/>
{!currentUser?.emailVerifiedAt ? (
<Button
type='button'
variant='outline'
onClick={handleVerifyEmail}
disabled={true}
>
{isUpdatingProfile ? (
<Loader2 className='h-4 w-4 animate-spin' />
) : (
<Mail className='h-4 w-4 mr-2' />
)}
Verify
</Button>
) : (
<Button variant='outline' disabled>
<Check className='h-4 w-4 mr-2' />
Verified
</Button>
)}
</div>
{updateProfileForm.formState.errors.email && (
<p className='text-sm text-destructive'>
{updateProfileForm.formState.errors.email.message}
</p>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='phone'>Phone Number</Label>
<Input
id='phone'
type='tel'
{...updateProfileForm.register('phone')}
placeholder='Enter your phone number'
defaultValue={currentUser?.phone}
/>
{updateProfileForm.formState.errors.phone && (
<p className='text-sm text-destructive'>
{updateProfileForm.formState.errors.phone.message}
</p>
)}
</div>
{isUpdateProfileSuccess && (
<p className='text-sm text-green-500'>
Profile updated successfully!
</p>
)}
<Button
type='submit'
className='w-full mt-6'
disabled={isUpdatingProfile}
>
{isUpdatingProfile ? (
<Loader2 className='h-4 w-4 animate-spin mr-2' />
) : null}
Save Changes
</Button>
</form>
)
}

View File

@@ -1,9 +1,9 @@
'use client'
import { useState } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { QrCode, Copy, AlertTriangle, Download, Smartphone } from 'lucide-react'
import { QrCode, Copy, AlertTriangle, Download, Smartphone, Lightbulb } from 'lucide-react'
import {
Dialog,
DialogContent,
@@ -23,7 +23,6 @@ export default function GetStartedCard() {
const [apiKey, setApiKey] = useState('')
const handleConfirmGenerateKey = () => {
setIsConfirmGenerateKeyModalOpen(true)
}
@@ -52,20 +51,53 @@ export default function GetStartedCard() {
return (
<>
<Card>
<CardHeader>
<CardTitle>Get Started</CardTitle>
<Card className="bg-gradient-to-br from-primary/10 to-background border-primary/20">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="rounded-full bg-primary/20 p-1.5">
<Lightbulb className="h-4 w-4 text-primary" />
</div>
<CardTitle>Quick Start Guide</CardTitle>
</div>
</div>
<CardDescription className="mt-2">
Complete these steps to start using TextBee SMS Gateway
</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
<p className='text-muted-foreground'>
To start using TextBee, you need to generate an API key and connect
your device.
</p>
<GenerateApiKey/>
<CardContent className="pt-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="flex items-start space-x-3">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-primary-foreground">
1
</div>
<div className="space-y-1">
<p className="font-medium">Download TextBee App</p>
<p className="text-sm text-muted-foreground">
Install the TextBee app on your Android device
</p>
<Button variant="outline" size="sm" className="mt-2" onClick={() => window.open('https://dl.textbee.dev', '_blank')}>
<Download className="mr-2 h-4 w-4" />
Download App APK
</Button>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-primary/20 text-primary">
2
</div>
<div className="space-y-1">
<p className="font-medium">Generate API Key</p>
<p className="text-sm text-muted-foreground">
Create an API key to authenticate your requests
</p>
<GenerateApiKey />
</div>
</div>
</div>
</CardContent>
</Card>
</>
)
}

View File

@@ -24,7 +24,7 @@ export default function DashboardOverview() {
onValueChange={handleTabChange}
className='space-y-4'
>
<TabsList className='sticky top-[4rem] z-10 flex mx-auto max-w-md border-[1px] my-6 bg-blue-500 text-white '>
<TabsList className='sticky top-[4rem] z-10 flex mx-auto max-w-md border-[1px] my-6 bg-brand-500 text-white '>
<TabsTrigger value='overview' className='flex-1'>
Overview
</TabsTrigger>

View File

@@ -397,6 +397,142 @@ function FollowUpDialog({
)
}
function StatusDetailsDialog({ message }: { message: any }) {
const [open, setOpen] = useState(false);
// Format timestamps for display
const formatTimestamp = (timestamp) => {
if (!timestamp) return 'N/A';
return new Date(timestamp).toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
day: 'numeric',
month: 'short',
year: 'numeric',
});
};
// Get status badge color and icon based on message status
const getStatusBadge = () => {
const status = message.status || 'pending';
switch (status) {
case 'PENDING':
return {
color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
icon: <Timer className="h-3 w-3 mr-1" />,
label: 'Pending'
};
case 'SENT':
return {
color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
icon: <Check className="h-3 w-3 mr-1" />,
label: 'Sent'
};
case 'DELIVERED':
return {
color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
icon: <Check className="h-3 w-3 mr-1" />,
label: 'Delivered'
};
case 'FAILED':
return {
color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
icon: <X className="h-3 w-3 mr-1" />,
label: 'Failed'
};
default:
return {
color: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300',
icon: <Timer className="h-3 w-3 mr-1" />,
label: status
};
}
};
const statusBadge = getStatusBadge();
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Badge variant="outline" className={`${statusBadge.color} flex items-center text-xs cursor-pointer`}>
{statusBadge.icon}
{statusBadge.label}
</Badge>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
SMS Status Details
</DialogTitle>
<DialogDescription>
Detailed information about this SMS message
</DialogDescription>
</DialogHeader>
<div className="space-y-4 mt-4">
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="font-medium">Status</div>
<div>{message.status || 'pending'}</div>
<div className="font-medium">Requested At</div>
<div>{formatTimestamp(message.requestedAt)}</div>
<div className="font-medium">Sent At</div>
<div>{formatTimestamp(message.sentAt)}</div>
<div className="font-medium">Delivered At</div>
<div>{formatTimestamp(message.deliveredAt)}</div>
{message.status === 'FAILED' && (
<>
<div className="font-medium">Failed At</div>
<div>{formatTimestamp(message.failedAt)}</div>
{message.errorCode && (
<>
<div className="font-medium">Error Code</div>
<div className="">{message.errorCode}</div>
</>
)}
{message.errorMessage && (
<>
<div className="font-medium">Error Message</div>
<div className="">{message.errorMessage}</div>
</>
)}
{(!message.errorCode && !message.errorMessage && message.error) && (
<>
<div className="font-medium">Error</div>
<div className="text-destructive">{message.error || 'Unknown error'}</div>
</>
)}
</>
)}
<div className="font-medium">Recipient</div>
<div>{message.recipient || (message.recipients && message.recipients[0]) || 'Unknown'}</div>
<div className="font-medium">Message ID</div>
<div className="font-mono text-xs">{message._id}</div>
{message.smsBatch && (
<>
<div className="font-medium">Batch ID</div>
<div className="font-mono text-xs">{message.smsBatch}</div>
</>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}
function MessageCard({ message, type }) {
const isSent = type === 'sent'
@@ -414,7 +550,7 @@ function MessageCard({ message, type }) {
<Card
className={`hover:bg-muted/50 transition-colors max-w-sm md:max-w-none ${
isSent
? 'border-l-4 border-l-blue-500'
? 'border-l-4 border-l-brand-500'
: 'border-l-4 border-l-green-500'
}`}
>
@@ -423,7 +559,7 @@ function MessageCard({ message, type }) {
<div className='flex justify-between items-start'>
<div className='flex items-center gap-2'>
{isSent ? (
<div className='flex items-center text-blue-600 dark:text-blue-400 font-medium'>
<div className='flex items-center text-brand-600 dark:text-brand-400 font-medium'>
<ArrowUpRight className='h-4 w-4 mr-1' />
<span>
To:{' '}
@@ -449,17 +585,18 @@ function MessageCard({ message, type }) {
<p className='text-sm max-w-sm md:max-w-none'>{message.message}</p>
</div>
{!isSent && (
<div className='flex justify-end'>
<ReplyDialog sms={message} />
<div className='flex justify-between items-center'>
{isSent && (
<div className='flex items-center'>
<StatusDetailsDialog message={message} />
</div>
)}
<div className='flex justify-end ml-auto'>
{!isSent && <ReplyDialog sms={message} />}
{isSent && <FollowUpDialog message={message} />}
</div>
)}
{isSent && (
<div className='flex justify-end'>
<FollowUpDialog message={message} />
</div>
)}
</div>
</div>
</CardContent>
</Card>
@@ -614,16 +751,16 @@ export default function MessageHistory() {
return (
<div className='space-y-4'>
<div className='bg-gradient-to-r from-blue-50 to-sky-50 dark:from-blue-950/30 dark:to-sky-950/30 rounded-lg shadow-sm border border-blue-100 dark:border-blue-800/50 p-4 mb-4'>
<div className='bg-gradient-to-r from-brand-50 to-sky-50 dark:from-brand-950/30 dark:to-sky-950/30 rounded-lg shadow-sm border border-brand-100 dark:border-brand-800/50 p-4 mb-4'>
<div className='flex flex-col gap-4'>
<div className='flex flex-col sm:flex-row sm:items-center gap-3'>
<div className='flex-1'>
<div className='flex items-center gap-2 mb-1.5'>
<Smartphone className='h-3.5 w-3.5 text-blue-500' />
<Smartphone className='h-3.5 w-3.5 text-brand-500' />
<h3 className='text-sm font-medium text-foreground'>Device</h3>
</div>
<Select value={currentDevice} onValueChange={handleDeviceChange}>
<SelectTrigger className='w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-blue-200 dark:border-blue-800/70'>
<SelectTrigger className='w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-brand-200 dark:border-brand-800/70'>
<SelectValue placeholder='Select a device' />
</SelectTrigger>
<SelectContent>
@@ -650,7 +787,7 @@ export default function MessageHistory() {
<div className='w-full sm:w-44'>
<div className='flex items-center gap-2 mb-1.5'>
<MessageSquare className='h-3.5 w-3.5 text-blue-500' />
<MessageSquare className='h-3.5 w-3.5 text-brand-500' />
<h3 className='text-sm font-medium text-foreground'>
Message Type
</h3>
@@ -659,7 +796,7 @@ export default function MessageHistory() {
value={messageType}
onValueChange={handleMessageTypeChange}
>
<SelectTrigger className='w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-blue-200 dark:border-blue-800/70'>
<SelectTrigger className='w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-brand-200 dark:border-brand-800/70'>
<SelectValue placeholder='Message type' />
</SelectTrigger>
<SelectContent>
@@ -677,7 +814,7 @@ export default function MessageHistory() {
</SelectItem>
<SelectItem value='sent'>
<div className='flex items-center gap-1.5'>
<div className='h-1.5 w-1.5 rounded-full bg-blue-500'></div>
<div className='h-1.5 w-1.5 rounded-full bg-brand-500'></div>
Sent
</div>
</SelectItem>
@@ -687,14 +824,14 @@ export default function MessageHistory() {
</div>
{/* Refresh Controls */}
<div className='flex items-center justify-between gap-2 pt-2 mt-2 border-t border-blue-100 dark:border-blue-800/50'>
<div className='flex items-center justify-between gap-2 pt-2 mt-2 border-t border-brand-100 dark:border-brand-800/50'>
<div className='flex items-center gap-1.5'>
<Button
onClick={handleRefresh}
variant='ghost'
size='sm'
disabled={!currentDevice}
className='h-7 px-2 text-xs text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/30'
className='h-7 px-2 text-xs text-brand-700 dark:text-brand-300 hover:bg-brand-100 dark:hover:bg-brand-900/30'
>
<RefreshCw
className={`h-3.5 w-3.5 mr-1 ${
@@ -712,7 +849,7 @@ export default function MessageHistory() {
</div>
<div className='flex items-center gap-1.5'>
<Timer className='h-3 w-3 text-blue-500' />
<Timer className='h-3 w-3 text-brand-500' />
<span className='text-xs font-medium mr-1'>Auto Refresh:</span>
<div className='flex'>
@@ -729,8 +866,8 @@ export default function MessageHistory() {
disabled={!currentDevice && interval.value > 0}
className={`h-6 px-1.5 text-xs ${
autoRefreshInterval === interval.value
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-medium'
: 'text-muted-foreground hover:bg-blue-50 dark:hover:bg-blue-900/20'
? 'bg-brand-100 dark:bg-brand-900/30 text-brand-700 dark:text-brand-300 font-medium'
: 'text-muted-foreground hover:bg-brand-50 dark:hover:bg-brand-900/20'
}`}
onClick={() => setAutoRefreshInterval(interval.value)}
>
@@ -792,7 +929,7 @@ export default function MessageHistory() {
size='icon'
className={`h-8 w-8 rounded-full ${
page === 1
? 'bg-primary text-primary-foreground hover:bg-primary/90'
? 'bg-primary text-brand-foreground hover:bg-primary/90'
: 'hover:bg-secondary'
}`}
>
@@ -835,7 +972,7 @@ export default function MessageHistory() {
size='icon'
className={`h-8 w-8 rounded-full ${
page === pageToShow
? 'bg-primary text-primary-foreground hover:bg-primary/90'
? 'bg-primary text-brand-foreground hover:bg-primary/90'
: 'hover:bg-secondary'
}`}
>
@@ -860,7 +997,7 @@ export default function MessageHistory() {
size='icon'
className={`h-8 w-8 rounded-full ${
page === pagination.totalPages
? 'bg-primary text-primary-foreground hover:bg-primary/90'
? 'bg-primary text-brand-foreground hover:bg-primary/90'
: 'hover:bg-secondary'
}`}
>

View File

@@ -15,7 +15,7 @@ export default function Messaging() {
}
return (
<div className='grid gap-6 max-w-sm md:max-w-xl mx-auto mt-10'>
<div className='grid gap-6 w-full max-w-sm md:max-w-3xl'>
<Tabs
value={currentTab}
onValueChange={handleTabChange}
@@ -38,7 +38,7 @@ export default function Messaging() {
</TabsContent>
<TabsContent value='bulk-send' className='space-y-4'>
<div className='grid gap-6 max-w-xl mx-auto mt-10'>
<div className='grid gap-6 w-full'>
<BulkSMSSend />
</div>
</TabsContent>

View File

@@ -1,7 +1,7 @@
'use client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { BarChart3, Smartphone, Key, MessageSquare } from 'lucide-react'
import { BarChart3, Smartphone, Key, MessageSquare, TrendingUp } from 'lucide-react'
import GetStartedCard from './get-started'
import { ApiEndpoints } from '@/config/api'
import httpBrowserClient from '@/lib/httpBrowserClient'
@@ -11,14 +11,21 @@ import { Skeleton } from '@/components/ui/skeleton'
export const StatCard = ({ title, value, icon: Icon, description }) => {
return (
<Card>
<Card className="overflow-hidden transition-all hover:shadow-md">
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>{title}</CardTitle>
<Icon className='h-4 w-4 text-muted-foreground' />
<div className="rounded-full bg-primary/10 p-2">
<Icon className='h-4 w-4 text-primary' />
</div>
</CardHeader>
<CardContent>
<div className='text-2xl font-bold'>{value ?? <Skeleton className='h-4 w-8 mb-2' />}</div>
<p className='text-xs text-muted-foreground'>{description}</p>
<div className='text-2xl font-bold'>
{value !== undefined ? value : <Skeleton className='h-6 w-16' />}
</div>
<p className='text-xs text-muted-foreground mt-1 flex items-center'>
{description}
{value !== undefined && <TrendingUp className="ml-1 h-3 w-3 text-green-500" />}
</p>
</CardContent>
</Card>
)
@@ -34,9 +41,9 @@ export default function Overview() {
})
return (
<div className='space-y-4'>
<div className='space-y-6'>
<GetStartedCard />
<div className='grid gap-4 grid-cols-2 lg:grid-cols-4'>
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>
<StatCard
title='Total SMS Sent'
value={stats?.totalSentSMSCount?.toLocaleString()}

View File

@@ -71,7 +71,7 @@ export default function SendSms() {
})
return (
<div className='grid gap-6 max-w-xl mx-auto mt-10'>
<div>
<Card>
<CardHeader>
<div className='flex items-center gap-2'>

View File

@@ -0,0 +1,266 @@
'use client'
import { Calendar, Check, Info } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Spinner } from '@/components/ui/spinner'
import { useQuery } from '@tanstack/react-query'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import Link from 'next/link'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
export default function SubscriptionInfo() {
const {
data: currentSubscription,
isLoading: isLoadingSubscription,
error: subscriptionError,
} = useQuery({
queryKey: ['currentSubscription'],
queryFn: () =>
httpBrowserClient
.get(ApiEndpoints.billing.currentSubscription())
.then((res) => res.data),
})
// Format price with currency symbol
const formatPrice = (
amount: number | null | undefined,
currency: string | null | undefined
) => {
if (amount == null || currency == null) return 'Free'
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase() || 'USD',
minimumFractionDigits: 2,
})
return formatter.format(amount / 100)
}
const getBillingInterval = (interval: string | null | undefined) => {
if (!interval) return ''
return interval.toLowerCase() === 'month' ? 'monthly' : 'yearly'
}
if (isLoadingSubscription)
return (
<div className='flex justify-center items-center h-full min-h-[200px]'>
<Spinner size='sm' />
</div>
)
if (subscriptionError)
return (
<p className='text-sm text-destructive'>
Failed to load subscription information
</p>
)
return (
<div className='bg-gradient-to-br from-white to-gray-50 dark:from-gray-800 dark:to-gray-900 border rounded-lg shadow p-5'>
<div className='flex items-center justify-between mb-5'>
<div>
<h3 className='text-xl font-bold text-gray-900 dark:text-white'>
{currentSubscription?.plan?.name || 'Free Plan'}
</h3>
<div className='flex items-center gap-2'>
<p className='text-sm text-gray-500 dark:text-gray-400'>
Current subscription
</p>
{currentSubscription?.amount > 0 && (
<Badge variant='outline' className='text-xs font-medium'>
{formatPrice(
currentSubscription?.amount,
currentSubscription?.currency
)}
{currentSubscription?.recurringInterval && (
<span className='ml-1'>
/{' '}
{getBillingInterval(currentSubscription?.recurringInterval)}
</span>
)}
</Badge>
)}
</div>
</div>
<div
className={`flex items-center px-2 py-0.5 rounded-full ${
currentSubscription?.status === 'active'
? 'bg-green-50 dark:bg-green-900/30'
: currentSubscription?.status === 'past_due'
? 'bg-amber-50 dark:bg-amber-900/30'
: 'bg-gray-50 dark:bg-gray-800/50'
}`}
>
<Check
className={`h-3 w-3 mr-1 ${
currentSubscription?.status === 'active'
? 'text-green-600 dark:text-green-400'
: currentSubscription?.status === 'past_due'
? 'text-amber-600 dark:text-amber-400'
: 'text-gray-600 dark:text-gray-400'
}`}
/>
<span
className={`text-xs font-medium ${
currentSubscription?.status === 'active'
? 'text-green-600 dark:text-green-400'
: currentSubscription?.status === 'past_due'
? 'text-amber-600 dark:text-amber-400'
: 'text-gray-600 dark:text-gray-400'
}`}
>
{currentSubscription?.status
? currentSubscription.status
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
: 'Active'}
</span>
</div>
</div>
<div className='grid grid-cols-2 gap-3 mb-5'>
<div className='flex items-center space-x-2 bg-white dark:bg-gray-800 p-3 rounded-md shadow-sm'>
<Calendar className='h-4 w-4 text-brand-600 dark:text-brand-400' />
<div>
<p className='text-xs text-gray-500 dark:text-gray-400'>
Start Date
</p>
<p className='text-sm font-medium text-gray-900 dark:text-white'>
{currentSubscription?.subscriptionStartDate
? new Date(
currentSubscription?.subscriptionStartDate
).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})
: 'N/A'}
</p>
</div>
</div>
<div className='flex items-center space-x-2 bg-white dark:bg-gray-800 p-3 rounded-md shadow-sm'>
<Calendar className='h-4 w-4 text-brand-600 dark:text-brand-400' />
<div>
<p className='text-xs text-gray-500 dark:text-gray-400'>
Next Payment
</p>
<p className='text-sm font-medium text-gray-900 dark:text-white'>
{currentSubscription?.currentPeriodEnd
? new Date(
currentSubscription?.currentPeriodEnd
).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})
: 'N/A'}
</p>
</div>
</div>
</div>
<div className='bg-white dark:bg-gray-800 p-4 rounded-md shadow-sm mb-5'>
<p className='text-xs text-gray-500 dark:text-gray-400 mb-3 font-medium'>
Usage Limits
</p>
<div className='grid grid-cols-3 gap-3'>
<div className='bg-gray-50 dark:bg-gray-700/50 p-2 rounded-md'>
<p className='text-xs text-gray-500 dark:text-gray-400'>Daily</p>
<p className='text-sm font-medium text-gray-900 dark:text-white'>
{currentSubscription?.plan?.dailyLimit === -1
? 'Unlimited'
: currentSubscription?.plan?.dailyLimit || '0'}
{currentSubscription?.plan?.dailyLimit === -1 && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className='inline-flex items-center'>
<Info className='h-4 w-4 text-gray-500 ml-1 cursor-pointer' />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Unlimited (within monthly limit)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</p>
</div>
<div className='bg-gray-50 dark:bg-gray-700/50 p-2 rounded-md'>
<p className='text-xs text-gray-500 dark:text-gray-400'>Monthly</p>
<p className='text-sm font-medium text-gray-900 dark:text-white'>
{currentSubscription?.plan?.monthlyLimit === -1
? 'Unlimited'
: currentSubscription?.plan?.monthlyLimit?.toLocaleString() ||
'0'}
{currentSubscription?.plan?.monthlyLimit === -1 && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className='inline-flex items-center'>
<Info className='h-4 w-4 text-gray-500 ml-1 cursor-pointer' />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Unlimited (within fair usage)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</p>
</div>
<div className='bg-gray-50 dark:bg-gray-700/50 p-2 rounded-md'>
<p className='text-xs text-gray-500 dark:text-gray-400'>Bulk</p>
<p className='text-sm font-medium text-gray-900 dark:text-white'>
{currentSubscription?.plan?.bulkSendLimit === -1
? 'Unlimited'
: currentSubscription?.plan?.bulkSendLimit || '0'}
{currentSubscription?.plan?.bulkSendLimit === -1 && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className='inline-flex items-center'>
<Info className='h-4 w-4 text-gray-500 ml-1 cursor-pointer' />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Unlimited (within monthly limit)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</p>
</div>
</div>
</div>
<div className='flex justify-end'>
{!currentSubscription?.plan?.name ||
currentSubscription?.plan?.name?.toLowerCase() === 'free' ? (
<Link
href='/checkout/pro'
className='text-sm font-medium text-white bg-brand-600 hover:bg-brand-700 px-4 py-2 rounded-md transition-colors'
>
Upgrade to Pro
</Link>
) : (
<Link
href='https://polar.sh/textbee/portal/'
className='text-sm font-medium text-white bg-brand-600 hover:bg-brand-700 px-4 py-2 rounded-md transition-colors'
>
Manage Subscription
</Link>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,209 @@
'use client'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { AlertTriangle, Check, Loader2 } from 'lucide-react'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { toast } from '@/hooks/use-toast'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { useRouter } from 'next/navigation'
const SupportFormSchema = z.object({
name: z.string().min(1, { message: 'Name is required' }),
email: z.string().email({ message: 'Invalid email address' }),
phone: z.string().optional(),
category: z.enum(['general', 'technical', 'billing-and-payments', 'other'], {
message: 'Support category is required',
}),
message: z.string().min(1, { message: 'Message is required' }),
})
export default function SupportForm() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSubmitSuccessful, setIsSubmitSuccessful] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const router = useRouter()
const form = useForm({
resolver: zodResolver(SupportFormSchema),
defaultValues: {
name: '',
email: '',
phone: '',
category: 'general',
message: '',
},
})
const onSubmit = async (data: any) => {
setIsSubmitting(true)
setErrorMessage(null)
try {
// Use the existing httpBrowserClient to call the NestJS endpoint
const response = await httpBrowserClient.post(
ApiEndpoints.support.customerSupport(),
data
)
setIsSubmitSuccessful(true)
toast({
title: 'Support request submitted',
description: response.data.message || 'We will get back to you soon.',
})
} catch (error) {
console.error('Error submitting support request:', error)
setErrorMessage(
'Error submitting support request. Please try again later.'
)
toast({
title: 'Error submitting support request',
description: 'Please try again later',
variant: 'destructive',
})
} finally {
setIsSubmitting(false)
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<FormField
control={form.control}
name='category'
disabled={isSubmitting}
render={({ field }) => (
<FormItem>
<FormLabel>Support Category</FormLabel>
<Select
onValueChange={field.onChange}
disabled={isSubmitting}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder='Select support category' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='general'>General Inquiry</SelectItem>
<SelectItem value='technical'>Technical Support</SelectItem>
<SelectItem value='billing-and-payments'>
Billing and Payments
</SelectItem>
<SelectItem value='other'>Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='name'
disabled={isSubmitting}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder='Your name' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='email'
disabled={isSubmitting}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder='your@email.com' type='email' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='phone'
disabled={isSubmitting}
render={({ field }) => (
<FormItem>
<FormLabel>Phone (Optional)</FormLabel>
<FormControl>
<Input placeholder='+1234567890' type='tel' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='message'
disabled={isSubmitting}
render={({ field }) => (
<FormItem>
<FormLabel>Message</FormLabel>
<FormControl>
<Textarea
placeholder='How can we help you?'
className='min-h-[100px]'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isSubmitSuccessful && (
<div className='flex items-center gap-2 text-green-500'>
<Check className='h-4 w-4' /> We have received your message, we will
get back to you soon.
</div>
)}
{errorMessage && (
<div className='flex items-center gap-2 text-red-500'>
<AlertTriangle className='h-4 w-4' /> {errorMessage}
</div>
)}
<Button type='submit' disabled={isSubmitting} className='w-full'>
{isSubmitting ? (
<>
<Loader2 className='h-4 w-4 animate-spin mr-2' /> Submitting...
</>
) : (
'Submit'
)}
</Button>
</form>
</Form>
)
}

View File

@@ -0,0 +1,29 @@
import { ShieldIcon } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import ChangePasswordForm from '../../(components)/change-password-form'
export default function ChangePasswordPage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<ShieldIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Change Password</h2>
</div>
<p className='text-muted-foreground'>Update your account password</p>
</div>
<div className='max-w-2xl'>
<Card>
<CardHeader>
<CardTitle>Password Security</CardTitle>
<CardDescription>Change your password to keep your account secure</CardDescription>
</CardHeader>
<CardContent>
<ChangePasswordForm />
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,34 @@
import { AlertTriangleIcon } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import DangerZoneForm from '../../(components)/danger-zone-form'
export default function DangerZonePage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<AlertTriangleIcon className='h-6 w-6 text-destructive' />
<h2 className='text-3xl font-bold tracking-tight'>Danger Zone</h2>
</div>
<p className='text-muted-foreground'>Manage critical account actions</p>
</div>
<div className='max-w-2xl'>
<Card className='border-destructive/50'>
<CardHeader>
<div className='flex items-center gap-2 text-destructive'>
<AlertTriangleIcon className='h-5 w-5' />
<CardTitle>Delete Account</CardTitle>
</div>
<CardDescription>
Permanently delete your account and all associated data
</CardDescription>
</CardHeader>
<CardContent>
<DangerZoneForm />
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
import { UserIcon } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import EditProfileForm from '../../(components)/edit-profile-form'
export default function EditProfilePage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<UserIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Edit Profile</h2>
</div>
<p className='text-muted-foreground'>Update your profile information</p>
</div>
<div className='max-w-2xl'>
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>Update your personal details</CardDescription>
</CardHeader>
<CardContent>
<EditProfileForm />
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
import { MessageSquareIcon } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import SupportForm from '../../(components)/support-form'
export default function GetSupportPage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<MessageSquareIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Get Support</h2>
</div>
<p className='text-muted-foreground'>Contact our support team for assistance</p>
</div>
<div className='max-w-2xl'>
<Card>
<CardHeader>
<CardTitle>Contact Support</CardTitle>
<CardDescription>Fill out the form below and we'll get back to you as soon as possible.</CardDescription>
</CardHeader>
<CardContent>
<SupportForm />
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,95 @@
import { UserIcon, PencilIcon, KeyIcon, AlertTriangleIcon, MessageSquareIcon } from 'lucide-react'
import SubscriptionInfo from '../(components)/subscription-info'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
export default function AccountPage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<UserIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Account</h2>
</div>
<p className='text-muted-foreground'>Manage your account settings and preferences</p>
</div>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-6'>
{/* Left column - Subscription Information */}
<div className='space-y-4'>
<h3 className='text-lg font-semibold'>Subscription Information</h3>
<SubscriptionInfo />
</div>
{/* Right column - Account Management */}
<div className='space-y-6'>
<div className='space-y-4'>
<h3 className='text-lg font-semibold'>Account Management</h3>
<div className='grid gap-4'>
<Link href="/dashboard/account/edit-profile">
<Button variant="outline" className="w-full justify-start h-auto py-3">
<div className='flex items-center'>
<div className='bg-primary/10 p-2 rounded-full mr-3'>
<PencilIcon className='h-5 w-5 text-primary' />
</div>
<div className='text-left'>
<div className='font-medium'>Edit Profile</div>
<div className='text-sm text-muted-foreground'>Update your personal information</div>
</div>
</div>
</Button>
</Link>
<Link href="/dashboard/account/change-password">
<Button variant="outline" className="w-full justify-start h-auto py-3">
<div className='flex items-center'>
<div className='bg-primary/10 p-2 rounded-full mr-3'>
<KeyIcon className='h-5 w-5 text-primary' />
</div>
<div className='text-left'>
<div className='font-medium'>Change Password</div>
<div className='text-sm text-muted-foreground'>Update your account password</div>
</div>
</div>
</Button>
</Link>
<Link href="/dashboard/account/get-support">
<Button variant="outline" className="w-full justify-start h-auto py-3">
<div className='flex items-center'>
<div className='bg-primary/10 p-2 rounded-full mr-3'>
<MessageSquareIcon className='h-5 w-5 text-primary' />
</div>
<div className='text-left'>
<div className='font-medium'>Get Support</div>
<div className='text-sm text-muted-foreground'>Contact our support team for assistance</div>
</div>
</div>
</Button>
</Link>
</div>
</div>
<div className='space-y-4 pt-4 border-t'>
<h3 className='text-lg font-semibold text-destructive'>Danger Zone</h3>
<Link href="/dashboard/account/delete-account">
<Button variant="outline" className="w-full justify-start h-auto py-3 border-destructive/30">
<div className='flex items-center'>
<div className='bg-destructive/10 p-2 rounded-full mr-3'>
<AlertTriangleIcon className='h-5 w-5 text-destructive' />
</div>
<div className='text-left'>
<div className='font-medium text-destructive'>Delete Account</div>
<div className='text-sm text-muted-foreground'>Permanently delete your account</div>
</div>
</div>
</Button>
</Link>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,20 @@
import { UsersIcon } from 'lucide-react'
import CommunityLinks from '../(components)/community-links'
export default function CommunityPage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<UsersIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Community</h2>
</div>
<p className='text-muted-foreground'>Connect with other users and find support</p>
</div>
<div className=''>
<CommunityLinks />
</div>
</div>
)
}

View File

@@ -1,13 +1,163 @@
import Dashboard from './(components)/dashboard-layout'
'use client'
import { Home, MessageSquareText, UserCircle, Users } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import AccountDeletionAlert from './(components)/account-deletion-alert'
import UpgradeToProAlert from './(components)/upgrade-to-pro-alert'
import VerifyEmailAlert from './(components)/verify-email-alert'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const pathname = usePathname()
return (
<Dashboard>
{children}
</Dashboard>
<div className='flex min-h-screen flex-col md:flex-row'>
{/* Sidebar for desktop */}
<aside className='hidden md:flex flex-col fixed top-[20%] left-0 w-24 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 shadow-lg z-10 rounded-r-lg'>
<nav className='flex flex-col justify-center items-center h-full py-3 space-y-4'>
<NavItem
href='/dashboard'
icon={<Home className='h-6 w-6 stroke-[1.5]' />}
label='Dashboard'
isActive={pathname === '/dashboard'}
/>
<NavItem
href='/dashboard/messaging'
icon={<MessageSquareText className='h-6 w-6 stroke-[1.5]' />}
label='Messaging'
isActive={pathname === '/dashboard/messaging'}
/>
<NavItem
href='/dashboard/community'
icon={<Users className='h-6 w-6 stroke-[1.5]' />}
label='Community'
isActive={pathname === '/dashboard/community'}
/>
<NavItem
href='/dashboard/account'
icon={<UserCircle className='h-6 w-6 stroke-[1.5]' />}
label='Account'
isActive={pathname === '/dashboard/account'}
/>
</nav>
</aside>
{/* Main content with left padding to account for fixed sidebar */}
<main className='flex-1 min-w-0 overflow-auto md:ml-24'>
<div className='space-y-2 p-4'>
<VerifyEmailAlert />
<AccountDeletionAlert />
<UpgradeToProAlert />
</div>
{children}
</main>
{/* Bottom navigation for mobile */}
<nav className='md:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 shadow-lg z-10'>
<div className='flex items-center justify-around h-16'>
<MobileNavItem
href='/dashboard'
icon={<Home className='h-5 w-5 stroke-[1.5]' />}
label='Dashboard'
isActive={pathname === '/dashboard'}
/>
<MobileNavItem
href='/dashboard/messaging'
icon={<MessageSquareText className='h-5 w-5 stroke-[1.5]' />}
label='Messaging'
isActive={pathname === '/dashboard/messaging'}
/>
<MobileNavItem
href='/dashboard/community'
icon={<Users className='h-5 w-5 stroke-[1.5]' />}
label='Community'
isActive={pathname === '/dashboard/community'}
/>
<MobileNavItem
href='/dashboard/account'
icon={<UserCircle className='h-5 w-5 stroke-[1.5]' />}
label='Account'
isActive={pathname === '/dashboard/account'}
/>
</div>
</nav>
{/* Bottom padding for mobile to account for the fixed navigation */}
<div className='h-16 md:hidden'></div>
</div>
)
}
// Desktop navigation item
function NavItem({
href,
icon,
label,
isActive,
}: {
href: string
icon: React.ReactNode
label: string
isActive: boolean
}) {
return (
<Link
href={href}
className={`flex flex-col items-center p-2 rounded-md transition-colors w-20 ${
isActive
? 'border border-brand-500 dark:border-brand-400 bg-brand-100/20 dark:bg-brand-900/10 text-brand-600 dark:text-brand-400'
: 'text-gray-700 dark:text-gray-200 hover:bg-brand-100/20 dark:hover:bg-brand-900/10 hover:text-brand-600 dark:hover:text-brand-400'
}`}
>
<span
className={
isActive
? 'text-brand-600 dark:text-brand-400 mb-1'
: 'text-gray-600 dark:text-gray-300 mb-1'
}
>
{icon}
</span>
<span className='font-medium text-xs'>{label}</span>
</Link>
)
}
// Mobile navigation item
function MobileNavItem({
href,
icon,
label,
isActive,
}: {
href: string
icon: React.ReactNode
label: string
isActive: boolean
}) {
return (
<Link
href={href}
className={`flex flex-col items-center justify-center p-2 rounded-md w-[23%] ${
isActive
? 'border border-brand-500 dark:border-brand-400 bg-brand-100/20 dark:bg-brand-900/10 text-brand-600 dark:text-brand-400'
: 'text-gray-700 dark:text-gray-300 hover:text-brand-600 dark:hover:text-brand-400'
}`}
>
<span
className={
isActive
? 'text-brand-600 dark:text-brand-400'
: 'text-gray-600 dark:text-gray-300'
}
>
{icon}
</span>
<span className='text-xs mt-1'>{label}</span>
</Link>
)
}

View File

@@ -0,0 +1,420 @@
'use client'
import { useState } from 'react'
import { Code, Terminal, Check, Copy, ArrowRight, ExternalLink, ChevronDown, ChevronUp } from 'lucide-react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../../../../components/ui/collapsible'
import SyntaxHighlighter from 'react-syntax-highlighter'
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import Link from 'next/link'
export default function ApiGuide() {
const [activeTab, setActiveTab] = useState('send-sms')
const [activeLangTab, setActiveLangTab] = useState('node')
const [copiedIndex, setCopiedIndex] = useState(-1)
const [isOpen, setIsOpen] = useState(false)
const handleCopy = (index: number, code: string) => {
navigator.clipboard.writeText(code)
setCopiedIndex(index)
setTimeout(() => setCopiedIndex(-1), 2000)
}
const apiEndpoints = [
{
id: 'send-sms',
title: 'Send SMS',
description: 'Send SMS messages to one or more recipients',
endpoint: '/api/v1/gateway/devices/:id/send-sms',
badge: { color: 'green', text: 'POST' },
request: {
node: {
language: 'javascript',
code: `import axios from 'axios'
const BASE_URL = 'https://api.textbee.dev/api/v1'
const API_KEY = 'YOUR_API_KEY'
const DEVICE_ID = 'YOUR_DEVICE_ID'
const response = await axios.post(
\`\${BASE_URL}/gateway/devices/\${DEVICE_ID}/send-sms\`,
{
recipients: [ '+1234567890' ],
message: 'Hello from TextBee!'
},
{ headers: { 'x-api-key': API_KEY } }
)
console.log(response.data)`
},
python: {
language: 'python',
code: `import requests
BASE_URL = 'https://api.textbee.dev/api/v1'
API_KEY = 'YOUR_API_KEY'
DEVICE_ID = 'YOUR_DEVICE_ID'
response = requests.post(
f'{BASE_URL}/gateway/devices/{DEVICE_ID}/send-sms',
json={
'recipients': ['+1234567890'],
'message': 'Hello from TextBee!'
},
headers={'x-api-key': API_KEY}
)
print(response.json())`
},
curl: {
language: 'bash',
code: `curl -X POST "https://api.textbee.dev/api/v1/gateway/devices/YOUR_DEVICE_ID/send-sms" \\
-H 'x-api-key: YOUR_API_KEY' \\
-H 'Content-Type: application/json' \\
-d '{
"recipients": [ "+1234567890" ],
"message": "Hello from TextBee!"
}'`
}
},
response: {
language: 'json',
code: `{
"data": {
"_id": "sms_1234567890",
"message": "Hello from TextBee!",
"recipients": ["+1234567890"],
"status": "PENDING",
"createdAt": "2023-09-15T14:23:45Z"
}
}`
}
},
{
id: 'get-sms',
title: 'Get SMS by ID',
description: 'Retrieve details and status of a specific SMS message',
endpoint: '/api/v1/gateway/devices/:id/sms/:smsId',
badge: { color: 'blue', text: 'GET' },
request: {
node: {
language: 'javascript',
code: `import axios from 'axios'
const BASE_URL = 'https://api.textbee.dev/api/v1'
const API_KEY = 'YOUR_API_KEY'
const DEVICE_ID = 'YOUR_DEVICE_ID'
const SMS_ID = 'YOUR_SMS_ID'
const response = await axios.get(
\`\${BASE_URL}/gateway/devices/\${DEVICE_ID}/sms/\${SMS_ID}\`,
{ headers: { 'x-api-key': API_KEY } }
)
console.log(response.data)`
},
python: {
language: 'python',
code: `import requests
BASE_URL = 'https://api.textbee.dev/api/v1'
API_KEY = 'YOUR_API_KEY'
DEVICE_ID = 'YOUR_DEVICE_ID'
SMS_ID = 'YOUR_SMS_ID'
response = requests.get(
f'{BASE_URL}/gateway/devices/{DEVICE_ID}/sms/{SMS_ID}',
headers={'x-api-key': API_KEY}
)
print(response.json())`
},
curl: {
language: 'bash',
code: `curl -X GET "https://api.textbee.dev/api/v1/gateway/devices/YOUR_DEVICE_ID/sms/YOUR_SMS_ID" \\
-H 'x-api-key: YOUR_API_KEY'`
}
},
response: {
language: 'json',
code: `{
"data": {
"_id": "sms_1234567890",
"message": "Hello from TextBee!",
"recipient": "+1234567890",
"status": "DELIVERED",
"sentAt": "2023-09-15T14:23:45Z",
"deliveredAt": "2023-09-15T14:23:48Z"
}
}`
}
},
{
id: 'get-batch',
title: 'Get SMS Batch',
description: 'Retrieve details and status of a batch of SMS messages',
endpoint: '/api/v1/gateway/devices/:id/sms-batch/:batchId',
badge: { color: 'blue', text: 'GET' },
request: {
node: {
language: 'javascript',
code: `import axios from 'axios'
const BASE_URL = 'https://api.textbee.dev/api/v1'
const API_KEY = 'YOUR_API_KEY'
const DEVICE_ID = 'YOUR_DEVICE_ID'
const BATCH_ID = 'YOUR_BATCH_ID'
const response = await axios.get(
\`\${BASE_URL}/gateway/devices/\${DEVICE_ID}/sms-batch/\${BATCH_ID}\`,
{ headers: { 'x-api-key': API_KEY } }
)
console.log(response.data)`
},
python: {
language: 'python',
code: `import requests
BASE_URL = 'https://api.textbee.dev/api/v1'
API_KEY = 'YOUR_API_KEY'
DEVICE_ID = 'YOUR_DEVICE_ID'
BATCH_ID = 'YOUR_BATCH_ID'
response = requests.get(
f'{BASE_URL}/gateway/devices/{DEVICE_ID}/sms-batch/{BATCH_ID}',
headers={'x-api-key': API_KEY}
)
print(response.json())`
},
curl: {
language: 'bash',
code: `curl -X GET "https://api.textbee.dev/api/v1/gateway/devices/YOUR_DEVICE_ID/sms-batch/YOUR_BATCH_ID" \\
-H 'x-api-key: YOUR_API_KEY'`
}
},
response: {
language: 'json',
code: `{
"data": {
"batch": {
"_id": "batch_9876543210",
"createdAt": "2023-09-15T14:23:45Z"
},
"messages": [
{
"_id": "sms_1234567890",
"recipient": "+1234567890",
"message": "Hello from TextBee!",
"status": "DELIVERED",
"sentAt": "2023-09-15T14:23:45Z",
"deliveredAt": "2023-09-15T14:23:48Z",
},
{
"_id": "sms_0987654321",
"recipient": "+0987654321",
"message": "Hello from TextBee!",
"status": "SENT",
"sentAt": "2023-09-15T14:23:45Z",
"deliveredAt": null
},{
"_id": "sms_0987654321",
"recipient": "+0987654321",
"message": "Hello from TextBee!",
"status": "FAILED",
"sentAt": null,
"deliveredAt": null,
"failedAt": "2023-09-15T14:23:45Z",
"errorCode": "1234567890",
"errorMessage": "Generic error"
}
]
}
}`
}
}
]
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="w-full">
<div className="border rounded-lg p-4 bg-card">
<CollapsibleTrigger asChild>
<div className="flex items-center justify-between cursor-pointer">
<div className="flex items-center gap-2">
<Code className="h-5 w-5 text-primary" />
<h3 className="text-xl font-semibold">API Documentation</h3>
</div>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</div>
</CollapsibleTrigger>
<CollapsibleContent className="mt-4">
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-muted-foreground">Integrate SMS capabilities into your applications</p>
<Link href="https://api.textbee.dev/" target="_blank">
<Button variant="outline" size="sm" className="flex items-center gap-1">
<ExternalLink className="h-4 w-4" />
<span>Full API Docs</span>
</Button>
</Link>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-3 mb-4">
{apiEndpoints.map((endpoint) => (
<TabsTrigger key={endpoint.id} value={endpoint.id}>
{endpoint.title}
</TabsTrigger>
))}
</TabsList>
{apiEndpoints.map((endpoint) => (
<TabsContent key={endpoint.id} value={endpoint.id}>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>{endpoint.title}</CardTitle>
<CardDescription>{endpoint.description}</CardDescription>
</div>
<Badge
variant="outline"
className={`
${endpoint.badge.color === 'green'
? 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800'
: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800'
}
`}
>
{endpoint.badge.text} {endpoint.endpoint}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Request Examples */}
<div className="space-y-2">
<div className="flex items-center">
<h4 className="text-sm font-medium">Request</h4>
<div className="ml-auto">
<Tabs value={activeLangTab} onValueChange={setActiveLangTab}>
<TabsList>
<TabsTrigger value="node" className="flex items-center gap-1">
<Code className="h-3.5 w-3.5" />
<span>Node.js</span>
</TabsTrigger>
<TabsTrigger value="python" className="flex items-center gap-1">
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor">
<path d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z" />
</svg>
<span>Python</span>
</TabsTrigger>
<TabsTrigger value="curl" className="flex items-center gap-1">
<Terminal className="h-3.5 w-3.5" />
<span>cURL</span>
</TabsTrigger>
</TabsList>
</Tabs>
</div>
</div>
<div className="relative">
{Object.entries(endpoint.request).map(([lang, data], index) => {
const codeIndex = apiEndpoints.indexOf(endpoint) * 10 + index
return (
<div key={lang} className={lang === activeLangTab ? 'block' : 'hidden'}>
<div className="absolute top-2 right-2 z-10">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 bg-slate-800/20 hover:bg-slate-800/30 dark:bg-white/10 dark:hover:bg-white/20 rounded-md"
onClick={() => handleCopy(codeIndex, data.code)}
>
{copiedIndex === codeIndex ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<SyntaxHighlighter
language={data.language}
showLineNumbers={data.language !== 'bash'}
style={dark}
customStyle={{
borderRadius: '0.5rem',
padding: '1.5rem',
fontSize: '0.875rem',
lineHeight: '1.5',
backgroundColor: '#1e293b', // slate-800
}}
>
{data.code}
</SyntaxHighlighter>
</div>
)
})}
</div>
</div>
{/* Response Example */}
<div className="space-y-2">
<h4 className="text-sm font-medium">Response</h4>
<div className="relative">
<div className="absolute top-2 right-2 z-10">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 bg-slate-800/20 hover:bg-slate-800/30 dark:bg-white/10 dark:hover:bg-white/20 rounded-md"
onClick={() => handleCopy(apiEndpoints.indexOf(endpoint) * 100, endpoint.response.code)}
>
{copiedIndex === apiEndpoints.indexOf(endpoint) * 100 ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<SyntaxHighlighter
language={endpoint.response.language}
showLineNumbers={true}
style={dark}
customStyle={{
borderRadius: '0.5rem',
padding: '1.5rem',
fontSize: '0.875rem',
lineHeight: '1.5',
backgroundColor: '#1e293b', // slate-800
}}
>
{endpoint.response.code}
</SyntaxHighlighter>
</div>
</div>
</CardContent>
<CardFooter className="border-t pt-6 flex justify-between">
<p className="text-sm text-muted-foreground">
For more details, see the full API documentation.
</p>
<Link href={`https://api.textbee.dev/#${endpoint.id}`} target="_blank">
<Button size="sm" variant="outline">
View Details
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</Link>
</CardFooter>
</Card>
</TabsContent>
))}
</Tabs>
</CollapsibleContent>
</div>
</Collapsible>
)
}

View File

@@ -0,0 +1,31 @@
'use client'
import { MessageSquareTextIcon } from 'lucide-react'
import Messaging from '../(components)/messaging'
import ApiGuide from './(components)/api-guide'
export default function MessagingPage() {
return (
<div className='flex-1 p-6 md:p-8'>
<div className='space-y-1 mb-6'>
<div className='flex items-center space-x-2'>
<MessageSquareTextIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Messaging</h2>
</div>
<p className='text-muted-foreground'>
Send messages and view your SMS history
</p>
</div>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-8'>
<div>
<Messaging />
</div>
<div>
<ApiGuide />
</div>
</div>
</div>
)
}

View File

@@ -1,3 +1,44 @@
'use client'
import DeviceList from './(components)/device-list'
import WebhooksSection from './(components)/webhooks/webhooks-section'
import Overview from './(components)/overview'
import ApiKeys from './(components)/api-keys'
import { useSession } from 'next-auth/react'
import { HomeIcon, ArrowUpRightIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
export default function DashboardPage() {
return <div>DashboardPage</div>
const { data: session } = useSession()
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-2'>
<HomeIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Dashboard</h2>
</div>
<Button variant="outline" size="sm" onClick={() => window.open('https://textbee.dev/quickstart', '_blank')}>
<ArrowUpRightIcon className="mr-2 h-4 w-4" />
Quick Start
</Button>
</div>
<p className='text-muted-foreground'>
Welcome back, {session?.user?.name || 'User'}
</p>
</div>
<div className='space-y-6'>
<Overview />
<div className='grid gap-6 md:grid-cols-2'>
<DeviceList />
<ApiKeys />
</div>
<WebhooksSection />
</div>
</div>
)
}

View File

@@ -1,100 +0,0 @@
'use client'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '../../../components/ui/tabs'
import SyntaxHighlighter from 'react-syntax-highlighter'
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism'
const codeSnippets = [
{
tech: 'NodeJs',
language: 'javascript',
snippet: `import axios from 'axios'
const BASE_URL = 'https://api.textbee.dev/api/v1'
const API_KEY = 'YOUR_API_KEY'
const DEVICE_ID = 'YOUR_DEVICE_ID'
const response = await axios.post(\`\$\{BASE_URL\}/gateway/devices/\$\{DEVICE_ID}/send-sms\`, {
recipients: [ '+1234567890' ],
message: 'Hello World!',
}, {
headers: {
'x-api-key': API_KEY,
},
})
console.log(response.data)`,
},
{
tech: 'Python',
language: 'python',
snippet: `import requests
BASE_URL = 'https://api.textbee.dev/api/v1'
API_KEY = 'YOUR_API_KEY'
DEVICE_ID = 'YOUR_DEVICE_ID'
response = requests.post(
f'{BASE_URL}/api/device/{DEVICE_ID}/send-sms',
json={
'recipients': ['+1234567890'],
'message': 'Hello World!'
},
headers={'x-api-key': API_KEY})
print(response.json())`,
},
{
tech: 'cURL',
language: 'bash',
snippet: `curl -X POST "https://api.textbee.dev/api/v1/gateway/devices/YOUR_DEVICE_ID/send-sms" \\
-H 'x-api-key: YOUR_API_KEY' \\
-H 'Content-Type: application/json' \\
-d '{
"recipients": [ "+1234567890" ],
"message": "Hello from textbee.dev"
}'`,
},
]
export default function CodeSnippetSection() {
return (
<section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl bg-gray-50 dark:bg-muted rounded-2xl my-12'>
<div className='mx-auto max-w-[58rem]'>
<h3 className='text-3xl font-bold mb-8'>Code Snippet</h3>
<div className='bg-white dark:bg-black p-6 rounded-xl shadow-sm'>
<Tabs defaultValue={codeSnippets[0].tech} className='w-full'>
<TabsList className='grid w-full grid-cols-3'>
{codeSnippets.map((snippet) => {
return (
<TabsTrigger key={snippet.tech} value={snippet.tech}>
{snippet.tech}
</TabsTrigger>
)
})}
</TabsList>
{codeSnippets.map((snippet) => {
return (
<TabsContent key={snippet.tech} value={snippet.tech}>
<SyntaxHighlighter
language={snippet.language}
showLineNumbers={snippet.language !== 'bash'}
style={dark}
// className='min-h-[200px]'
>
{snippet.snippet}
</SyntaxHighlighter>
</TabsContent>
)
})}
</Tabs>
</div>
</div>
</section>
)
}

View File

@@ -1,49 +0,0 @@
import { ArrowRight } from 'lucide-react'
import { Button } from '../../../components/ui/button'
import Link from 'next/link'
export default function CustomizationSection() {
return (
<section className='py-24 bg-gradient-to-b from-blue-50 to-white dark:from-blue-950 dark:to-muted'>
<div className='container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl'>
<div className='mx-auto max-w-4xl text-center mb-12'>
<h2 className='text-4xl font-bold mb-4 text-blue-600'>
Custom Development Solutions
</h2>
<p className='text-xl text-gray-600 mb-8'>
Need help with TextBee or other development projects? We offer expertise in:
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10 text-center">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold text-blue-600 mb-3">Self-Hosting Setup</h3>
<p className="text-gray-600 dark:text-gray-300">Get assistance deploying TextBee on your own infrastructure.</p>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold text-blue-600 mb-3">Custom Integrations</h3>
<p className="text-gray-600 dark:text-gray-300">Integrate TextBee with your existing applications or workflows.</p>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold text-blue-600 mb-3">Development Projects</h3>
<p className="text-gray-600 dark:text-gray-300">Collaborate with our team on your software development needs beyond TextBee.</p>
</div>
</div>
<Link
href={`mailto:contact@textbee.dev?subject=Custom Development Inquiry&body=I'm interested in discussing the following custom solution:%0A%0A- [ ] Self-hosting setup%0A- [ ] Custom integrations%0A- [ ] Other development project%0A%0AProject details:%0A%0A`}
>
<Button
size='lg'
className='bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 px-8 rounded-full shadow-lg transition-all duration-300 ease-in-out transform hover:scale-105'
>
Let's Discuss Your Project
<ArrowRight className='ml-2 h-5 w-5' />
</Button>
</Link>
</div>
</div>
</section>
)
}

View File

@@ -1,36 +0,0 @@
import Image from 'next/image'
import { Button } from '../../../components/ui/button'
import Link from 'next/link'
import { Routes } from '@/config/routes'
export default function DownloadAppSection() {
return (
<section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl'>
<div className='mx-auto max-w-[58rem] text-center'>
<div className='rounded-2xl bg-gradient-to-r from-blue-50 to-indigo-50 p-8 dark:from-blue-950 dark:to-muted'>
<div className='mx-auto max-w-sm'>
<Image
alt='App preview'
className='mx-auto mb-8 rounded-xl shadow-lg'
height='400'
src='/images/smsgatewayandroid.png'
width='200'
/>
<h3 className='text-xl font-bold mb-2'>
Download the App to get started!
</h3>
<p className='text-gray-500 mb-4'>
Unlock the power of messaging with our open-source Android SMS
Gateway.
</p>
<Link href={Routes.downloadAndroidApp} prefetch={false}>
<Button className='bg-blue-500 hover:bg-blue-600 text-white'>
Download App
</Button>
</Link>
</div>
</div>
</div>
</section>
)
}

View File

@@ -1,48 +0,0 @@
import { Card } from '@/components/ui/card'
import { Code, Send, Zap, Users } from 'lucide-react'
export default function FeaturesSection() {
return (
<section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl'>
<div className='mx-auto flex max-w-[58rem] flex-col items-center justify-center gap-4 text-center'>
<h2 className='text-3xl font-bold'>Features</h2>
<p className='max-w-[85%] text-gray-500'>
The ultimate solution for your messaging needs! Our free open-source
Android-based SMS Gateway provides you with all the features you need
to effectively manage your SMS communications.
</p>
</div>
<div className='mx-auto grid justify-center gap-6 sm:grid-cols-2 md:max-w-[64rem] md:grid-cols-4 mt-12'>
<Card className='flex flex-col items-center justify-center p-6 text-center'>
<Send className='h-12 w-12 mb-4 text-blue-500' />
<h3 className='font-bold'>Send SMS</h3>
<p className='text-sm '>
Send SMS to any number from your dashboard or via REST API
</p>
</Card>
<Card className='flex flex-col items-center justify-center p-6 text-center'>
<Users className='h-12 w-12 mb-4 text-blue-500' />
<h3 className='font-bold'>Bulk SMS</h3>
<p className='text-sm text-gray-500'>
Send SMS to multiple numbers at once
</p>
</Card>
<Card className='flex flex-col items-center justify-center p-6 text-center'>
<Zap className='h-12 w-12 mb-4 text-blue-500' />
<h3 className='font-bold'>Free</h3>
<p className='text-sm text-gray-500'>
No credit card required to get started.
</p>
</Card>
<Card className='flex flex-col items-center justify-center p-6 text-center'>
<Code className='h-12 w-12 mb-4 text-blue-500' />
<h3 className='font-bold'>Open Source</h3>
<p className='text-sm text-gray-500'>
The entire codebase is open source and available on GitHub.
</p>
</Card>
</div>
</section>
)
}

View File

@@ -1,75 +0,0 @@
import { Button } from '@/components/ui/button'
import { Routes } from '@/config/routes'
import { Smartphone, Code, Zap } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
export default function HeroSection() {
return (
<section className='relative overflow-hidden bg-gradient-to-b from-blue-50 to-white dark:from-blue-950 dark:to-muted py-16 sm:py-24'>
<div className='absolute inset-0 bg-[url(/grid.svg)] bg-center [mask-image:linear-gradient(180deg,white,rgba(255,255,255,0))]'></div>
<div className='container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative'>
<div className='grid gap-8 lg:grid-cols-2 lg:gap-16'>
<div className='flex flex-col justify-center space-y-8'>
<div className='space-y-4'>
<h1 className='text-4xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none'>
Transform Your Android into a
<span className='text-blue-500 block'>
{' '}
Powerful SMS Gateway
</span>
</h1>
<p className='max-w-[600px] text-gray-500 md:text-xl'>
Unlock the potential of your device with our open-source
solution. Send SMS effortlessly through your applications.
</p>
</div>
<div className='flex flex-cdol gap-4 flex-row'>
<Link href={Routes.register} prefetch={false}>
<Button className='bg-blue-500 hover:bg-blue-600 dark:text-white' size='lg'>
Get Started
</Button>
</Link>
<a href='#how-it-works'>
<Button variant='outline' size='lg'>
How It Works
</Button>
</a>
</div>
<div className='flex items-center space-x-4 text-sm'>
<div className='flex items-center'>
<Smartphone className='mr-2 h-4 w-4 text-blue-500' />
Android Compatible
</div>
<div className='flex items-center'>
<Code className='mr-2 h-4 w-4 text-blue-500' />
Open Source
</div>
<div className='flex items-center'>
<Zap className='mr-2 h-4 w-4 text-blue-500' />
Easy Setup
</div>
</div>
</div>
<div className='relative mx-auto w-full max-w-lg lg:max-w-none'>
<div className='absolute -top-4 -right-4 h-72 w-72 bg-blue-100 rounded-full blur-3xl'></div>
<div className='absolute -bottom-4 -left-4 h-72 w-72 bg-blue-100 rounded-full blur-3xl'></div>
<div className='relative'>
<Image
alt='TextBee App'
className='relative mx-auto w-full max-w-lg rounded-2xl shadow-xl'
height='600'
src='/images/smsgatewayandroid.png'
style={{
objectFit: 'contain',
}}
width='500'
/>
<div className='absolute inset-0 rounded-2xl bg-gradient-to-tr from-blue-400 to-blue-300 opacity-20'></div>
</div>
</div>
</div>
</div>
</section>
)
}

View File

@@ -1,57 +0,0 @@
import { Routes } from '@/config/routes'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '../../../components/ui/accordion'
export default function HowItWorksSection() {
return (
<section
id='how-it-works'
className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl bg-gray-50 dark:bg-muted rounded-2xl'
>
<div className='mx-auto max-w-[58rem]'>
<h2 className='text-3xl font-bold text-center mb-8'>How It Works</h2>
<p className='text-center mb-12 text-gray-500'>
How it works is simple. You install the app on your Android device,
and it will turn your device into a SMS Gateway. You can then use the
API to send SMS messages from your web applications.
</p>
<Accordion type='single' collapsible className='w-full'>
<AccordionItem value='step-1'>
<AccordionTrigger>
Step 1: Download The Android App
</AccordionTrigger>
<AccordionContent>
Download the Android App from{' '}
<a href={Routes.downloadAndroidApp} target='_blank'>
{Routes.downloadAndroidApp}
</a>
</AccordionContent>
</AccordionItem>
<AccordionItem value='step-2'>
<AccordionTrigger>Step 2: Generate an API key</AccordionTrigger>
<AccordionContent>
Generate an API key from the dashboard
</AccordionContent>
</AccordionItem>
<AccordionItem value='step-3'>
<AccordionTrigger>Step 3: Scan the QR code</AccordionTrigger>
<AccordionContent>
Open the textbee mobile app and scan the QR code or enter your api
key manually and enable the gateway app
</AccordionContent>
</AccordionItem>
<AccordionItem value='step-4'>
<AccordionTrigger>Step 4: Start sending</AccordionTrigger>
<AccordionContent>
Start sending SMS messages from the dashboard or using the API
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</section>
)
}

View File

@@ -1,91 +0,0 @@
import Link from 'next/link'
import {
MessageSquarePlus,
Moon,
CreditCard,
Heart,
LayoutDashboard,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Routes } from '@/config/routes'
import { ThemeProvider } from 'next-themes'
import ThemeToggle from '@/components/shared/theme-toggle'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
export default function LandingPageHeader() {
return (
<ThemeProvider attribute='class' defaultTheme='system'>
<header className='sticky top-0 z-50 w-full border-b bg-white/95 dark:bg-[#1A2752] backdrop-blur supports-[backdrop-filter]:bg-white/60'>
<div className='container flex h-14 items-center justify-between px-2'>
<Link
className='flex items-center space-x-2'
href={Routes.landingPage}
>
<MessageSquarePlus className='h-6 w-6 text-blue-500' />
<span className='font-bold'>
Text<span className='text-blue-500'>Bee</span>
<span className='text-xs align-center text-gray-500 dark:text-gray-400'>
.dev
</span>
</span>
</Link>
<nav className='flex items-center space-x-4'>
<ThemeToggle />
<TooltipProvider>
<Link
className='text-sm font-medium hover:text-blue-500'
href={'/#pricing'}
>
<span className='hidden sm:inline'>Pricing</span>
<Tooltip>
<TooltipTrigger asChild>
<CreditCard className='h-5 w-5 sm:hidden' />
</TooltipTrigger>
<TooltipContent>
<p>Pricing</p>
</TooltipContent>
</Tooltip>
</Link>
<Link
className='text-sm font-medium hover:text-blue-500'
href={Routes.contribute}
>
<span className='hidden sm:inline'>Contribute</span>
<Tooltip>
<TooltipTrigger asChild>
<Heart className='h-5 w-5 sm:hidden' />
</TooltipTrigger>
<TooltipContent>
<p>Contribute</p>
</TooltipContent>
</Tooltip>
</Link>
<Link
className='text-sm font-medium hover:text-blue-500'
href={Routes.dashboard}
>
<Button className='bg-blue-500 hover:bg-blue-600 dark:text-white rounded-full'>
<span className='hidden sm:inline'>Go to Dashboard</span>
<Tooltip>
<TooltipTrigger asChild>
<LayoutDashboard className='h-5 w-5 sm:hidden' />
</TooltipTrigger>
<TooltipContent>
<p>Go to Dashboard</p>
</TooltipContent>
</Tooltip>
</Button>
</Link>
</TooltipProvider>
</nav>
</div>
</header>
</ThemeProvider>
)
}

View File

@@ -1,195 +0,0 @@
'use client'
import { Button } from '@/components/ui/button'
import { Check } from 'lucide-react'
import Link from 'next/link'
const PricingSection = () => {
return (
<section
id='pricing'
className='py-16 bg-gradient-to-b from-white to-gray-50 dark:from-gray-900 dark:to-gray-950'
>
<div className='container px-4 mx-auto'>
<div className='max-w-2xl mx-auto mb-12 text-center'>
<h2 className='text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl'>
Pricing
</h2>
<p className='mt-3 text-base text-gray-600 dark:text-gray-400'>
Choose the perfect plan for your messaging needs
</p>
</div>
<div className='grid gap-6 lg:grid-cols-3'>
{/* Free Plan */}
<div className='flex flex-col p-5 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow'>
<h3 className='text-xl font-bold text-gray-900 dark:text-white'>
Free
</h3>
<p className='mt-3 text-sm text-gray-600 dark:text-gray-400'>
Perfect for getting started
</p>
<div className='my-6'>
<span className='text-3xl font-bold text-gray-900 dark:text-white'>
$0
</span>
<span className='text-gray-600 dark:text-gray-400'>/month</span>
</div>
<ul className='mb-6 space-y-3 flex-1'>
<Feature text='Send and receive SMS Messages' />
<Feature text='Register 1 active device' />
<Feature text='Max 50 messages per day' />
<Feature text='Up to 500 messages per month' />
<Feature text='Up to 50 recipients in bulk' />
<Feature text='Webhook notifications' />
<Feature text='Basic support' />
</ul>
<Button asChild className='w-full' variant='outline'>
<Link href='/dashboard?selectedPlan=free'>Get Started</Link>
</Button>
</div>
{/* Pro Plan */}
<div className='flex flex-col p-5 bg-slate-800 dark:bg-gray-800/60 text-white rounded-lg border border-gray-800 dark:border-gray-600 shadow-lg scale-105 hover:scale-105 transition-transform'>
<div className='inline-block px-3 py-1 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 text-xs font-semibold mb-3'>
MOST POPULAR
</div>
<h3 className='text-xl font-bold'>Pro</h3>
<p className='mt-3 text-sm text-gray-300'>
Ideal for most use-cases
</p>
<div className='my-6'>
<div className='grid grid-cols-2 gap-2'>
{/* Monthly pricing */}
<div className='space-y-2'>
<div className='flex items-baseline'>
<span className='text-xs text-gray-400 uppercase'>
Monthly
</span>
</div>
<div>
<div className='space-y-1'>
<div className='text-lg text-gray-400 line-through'>
$9.99
</div>
<div className='flex items-baseline gap-1'>
<span className='text-3xl font-bold'>$6.99</span>
<span className='text-gray-300'>/month</span>
</div>
</div>
<span className='mt-1 inline-block bg-green-500/10 text-green-400 text-xs px-2 py-0.5 rounded-full border border-green-500/20'>
Save 30%
</span>
</div>
</div>
{/* Yearly pricing */}
<div className='space-y-2 border-l border-gray-800 pl-2'>
<div className='flex items-baseline gap-2'>
<span className='text-xs text-gray-400 uppercase'>
Yearly
</span>
<span className='text-xs text-green-400'>
(2 months free)
</span>
</div>
<div>
<div className='space-y-1'>
<div className='text-lg text-gray-400 line-through'>
$99
</div>
<div className='flex items-baseline gap-1'>
<span className='text-3xl font-bold'>$69</span>
<span className='text-gray-300'>/year</span>
</div>
</div>
<span className='mt-1 inline-block bg-green-500/10 text-green-400 text-xs px-2 py-0.5 rounded-full border border-green-500/20'>
Save 42%
</span>
</div>
</div>
</div>
</div>
<ul className='mb-6 space-y-3 flex-1'>
<Feature text='Everything in Free' light />
<Feature text='Register upto 5 active devices' light />
<Feature
text='Unlimited daily messages (within monthly quota)'
light
/>
<Feature text='Up to 5,000 messages per month' light />
<Feature text='No bulk SMS recipient limits' light />
<Feature text='Priority support' light />
</ul>
<Button
asChild
className='w-full bg-white text-black hover:bg-gray-100'
>
<Link href='/checkout/pro'>Upgrade to Pro</Link>
</Button>
</div>
{/* Custom Plan */}
<div className='flex flex-col p-5 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow'>
<h3 className='text-xl font-bold text-gray-900 dark:text-white'>
Custom
</h3>
<p className='mt-3 text-sm text-gray-600 dark:text-gray-400'>
For more specific needs or custom integrations
</p>
<div className='my-6'>
<span className='text-3xl font-bold text-gray-900 dark:text-white'>
Custom
</span>
<span className='text-gray-600 dark:text-gray-400'> pricing</span>
</div>
<ul className='mb-6 space-y-3 flex-1'>
<Feature text='Custom message limits' />
<Feature text='Custom bulk limits' />
<Feature text='Custom integrations' />
<Feature text='SLA agreement' />
<Feature text='Dedicated support' />
</ul>
<Button asChild className='w-full' variant='outline'>
<Link href='mailto:sales@textbee.dev?subject=Interested%20in%20TextBee%20Custom%20Plan'>
Contact Sales
</Link>
</Button>
</div>
</div>
</div>
</section>
)
}
const Feature = ({
text,
light = false,
}: {
text: string
light?: boolean
}) => (
<li className='flex items-center'>
<Check
className={`h-4 w-4 ${
light ? 'text-green-400' : 'text-green-500 dark:text-green-400'
} mr-2`}
/>
<span
className={`text-sm ${
light ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'
}`}
>
{text}
</span>
</li>
)
export default PricingSection

View File

@@ -1,116 +0,0 @@
'use client'
import { toast } from '@/hooks/use-toast'
import { Button } from '../../../components/ui/button'
import { Heart, Coins, Check, Copy, Star, CreditCard } from 'lucide-react'
import { useState } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '../../../components/ui/dialog'
import Link from 'next/link'
import { ExternalLinks } from '@/config/external-links'
import { CRYPTO_ADDRESSES } from '@/lib/constants'
import Image from 'next/image'
export default function SupportProjectSection() {
const [cryptoOpen, setCryptoOpen] = useState(false)
const [copiedAddress, setCopiedAddress] = useState('')
const copyToClipboard = (address: string) => {
navigator.clipboard.writeText(address)
setCopiedAddress(address)
toast({
title: 'Address copied!',
description: 'The wallet address has been copied to your clipboard.',
})
setTimeout(() => setCopiedAddress(''), 3000)
}
return (
<>
<section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl bg-gray-50 dark:bg-muted rounded-2xl my-12'>
<div className='mx-auto max-w-[58rem] text-center'>
<h2 className='text-3xl font-bold mb-4'>Support The Project</h2>
<p className='text-gray-500 mb-8'>
Maintaining an open-source project requires time and dedication.
Your contribution will directly support the development, including
implementation of new features, enhance performance, and ensure the
highest level of security and reliability.
</p>
<div className='flex flex-col sm:flex-row justify-center gap-4 flex-wrap'>
<Link href={ExternalLinks.patreon} prefetch={false} target='_blank'>
<Button className='bg-blue-500 hover:bg-blue-600 text-white sm:w-auto w-full'>
<Heart className='mr-2 h-4 w-4' /> Become a Patron
</Button>
</Link>
<Link href={ExternalLinks.github} prefetch={false} target='_blank'>
<Button variant='outline' className='sm:w-auto w-full'>
<Star className='mr-2 h-4 w-4' /> Star on GitHub
</Button>
</Link>
<Link href={ExternalLinks.polar} prefetch={false} target='_blank'>
<Button variant='outline' className='sm:w-auto w-full'>
<CreditCard className='mr-2 h-4 w-4' /> One-time Donation
</Button>
</Link>
<Button
variant='outline'
onClick={() => setCryptoOpen(true)}
className='sm:w-auto w-full'
>
<Coins className='mr-2 h-4 w-4' /> Donate Crypto
</Button>
</div>
</div>
</section>
<Dialog open={cryptoOpen} onOpenChange={setCryptoOpen}>
<DialogContent className='sm:max-w-[500px]'>
<DialogHeader>
<DialogTitle>Donate Cryptocurrency</DialogTitle>
</DialogHeader>
<div className='grid gap-2 py-2'>
{CRYPTO_ADDRESSES.map((wallet, index) => (
<div
key={index}
className='flex items-center justify-between p-2 rounded-lg bg-gray-100 dark:bg-muted'
>
<div className='flex items-center gap-2'>
<Image
src={wallet.icon}
alt={wallet.name}
width={24}
height={24}
/>
<div>
<div className='flex items-center gap-2'>
<h4 className='font-medium text-sm'>{wallet.name}</h4>
<span className='text-xs text-gray-500'>({wallet.network})</span>
</div>
<p className='text-xs text-gray-400 break-all'>
{wallet.address}
</p>
</div>
</div>
<Button
variant='ghost'
size='sm'
onClick={() => copyToClipboard(wallet.address)}
className='h-8 w-8'
>
{copiedAddress === wallet.address ? (
<Check className='h-4 w-4' />
) : (
<Copy className='h-4 w-4' />
)}
</Button>
</div>
))}
</div>
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -1,11 +0,0 @@
import { PropsWithChildren } from 'react'
import LandingPageHeader from './(components)/landing-page-header'
export default async function RootLayout({ children }: PropsWithChildren) {
return (
<>
<LandingPageHeader />
{children}
</>
)
}

View File

@@ -1,24 +0,0 @@
import DownloadAppSection from '@/app/(landing-page)/(components)/download-app-section'
import FeaturesSection from '@/app/(landing-page)/(components)/features-section'
import HeroSection from '@/app/(landing-page)/(components)/hero-section'
import HowItWorksSection from '@/app/(landing-page)/(components)/how-it-works-section'
import CustomizationSection from '@/app/(landing-page)/(components)/customization-section'
import SupportProjectSection from '@/app/(landing-page)/(components)/support-project-section'
import CodeSnippetSection from '@/app/(landing-page)/(components)/code-snippet-section'
import PricingSection from '@/app/(landing-page)/(components)/pricing-section'
export default function LandingPage() {
return (
<div className='flex min-h-screen flex-col'>
<main className='flex-1'>
<HeroSection />
<FeaturesSection />
<HowItWorksSection />
<DownloadAppSection />
<PricingSection />
<CustomizationSection />
<CodeSnippetSection />
<SupportProjectSection />
</main>
</div>
)
}

View File

@@ -1,80 +0,0 @@
import { Metadata } from 'next'
import { Card, CardContent } from '@/components/ui/card'
export const metadata: Metadata = {
title: 'Privacy Policy | TextBee',
description: 'Privacy Policy for TextBee SMS Gateway Platform',
}
export default function PrivacyPolicyPage() {
return (
<>
<div className='container max-w-7xl py-6 md:px-12'>
<Card className='border-none shadow-none'>
<CardContent className='space-y-6'>
<h1 className='scroll-m-20 text-4xl font-bold tracking-tight lg:text-5xl'>
Privacy Policy
</h1>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
Effective Date: May 2022
</h2>
<p className='leading-7 [&:not(:first-child)]:mt-6'>
Thank you for using our TextBee SMS Gateway Platform
(&ldquo;Platform&rdquo;). This Privacy Policy is intended to
inform you about how we collect, use, and disclose information
when you use our Platform. We are committed to protecting your
privacy and ensuring the security of your personal information. By
using our Platform, you consent to the practices described in this
Privacy Policy.
</p>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
1. Information We Collect
</h2>
<h3 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
1.1 Personal Information:
</h3>
<p className='leading-7'>
We may collect the following types of personal information from
you when you use our Platform:
</p>
<ul className='my-6 ml-6 list-disc [&>li]:mt-2'>
<li>Your name</li>
<li>
Contact information (such as email address and phone number)
</li>
<li>
Device information (such as device ID, model, and operating
system)
</li>
<li>SMS content and metadata</li>
</ul>
</div>
{/* ... Continue with other sections ... */}
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
8. Contact Us
</h2>
<p className='leading-7'>
If you have any questions or concerns about this Privacy Policy
or our data practices, please contact us at{' '}
<a
href='mailto:contact@textbee.dev'
className='font-medium text-primary underline underline-offset-4 hover:text-primary/80'
>
contact@textbee.dev
</a>
.
</p>
</div>
</CardContent>
</Card>
</div>
</>
)
}

View File

@@ -1,581 +0,0 @@
import { Metadata } from 'next'
import Link from 'next/link'
import {
CheckCircle2,
Smartphone,
MessageSquare,
Settings,
ArrowRightCircle,
Zap,
ExternalLink,
BookOpen,
Star,
SparkleIcon,
} from 'lucide-react'
export const metadata: Metadata = {
title: 'TextBee Quickstart - Send SMS from Your Android Phone | SMS Gateway',
description:
'Get started with TextBee SMS Gateway in minutes. Learn how to send and receive SMS messages using your Android phone as an SMS gateway for your applications.',
keywords:
'SMS gateway, Android SMS, API SMS, TextBee quickstart, SMS integration, two-way SMS',
}
export default function QuickstartPage() {
return (
<div className='container max-w-5xl mx-auto py-12 px-4 md:px-8'>
<div className='mb-12 bg-gradient-to-r from-primary/10 to-transparent rounded-lg p-6 md:p-8 border border-primary/20 text-center mx-auto'>
<div className='inline-flex items-center gap-2 px-3 py-1 bg-primary/10 rounded-full text-primary text-sm font-medium mb-4'>
<Zap className='h-3.5 w-3.5' />
<span>5-minute setup</span>
</div>
<h1 className='text-3xl md:text-4xl font-bold mb-4 tracking-tight text-center'>
TextBee SMS Gateway Quickstart
</h1>
<p className='text-lg text-muted-foreground mb-4 mx-auto max-w-2xl text-center'>
Transform your Android phone into a powerful SMS gateway in just 5
minutes. Send and receive text messages programmatically through your
applications with TextBee.
</p>
<p className='text-sm text-muted-foreground mb-6 mx-auto max-w-2xl text-center'>
Our platform enables businesses and developers to implement SMS
functionality without expensive telecom infrastructure. Perfect for
notifications, authentication, alerts, and customer engagement.
</p>
<div className='flex flex-wrap gap-4 justify-center'>
<a
href='https://dl.textbee.dev'
className='inline-flex items-center gap-2 px-5 py-2.5 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors'
>
<Smartphone className='h-4 w-4' />
<span>Download App</span>
</a>
<a
href='#pro-plan'
className='inline-flex items-center gap-2 px-5 py-2.5 border border-primary/30 text-primary rounded-md hover:bg-primary/10 transition-colors'
>
<Star className='h-4 w-4' />
<span>View Pro Plan</span>
</a>
</div>
</div>
<div className='hidden md:block relative z-0 mb-12 mx-auto'>
<div className='absolute top-4 left-4 right-4 h-0.5 bg-muted'></div>
<div className='flex justify-between items-start px-4 relative z-10'>
{[1, 2, 3, 4, 5].map((step) => (
<div key={step} className='flex flex-col items-center'>
<a
href={`#step-${step}`}
className='w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium hover:bg-primary/20 transition-colors mb-2'
>
{step}
</a>
<span className='text-xs text-muted-foreground hidden sm:block'>
Step {step}
</span>
</div>
))}
</div>
</div>
<div className='mb-10 text-center mx-auto'>
<h2 className='text-2xl font-semibold mb-3 text-center'>
The Simplest Way to Add SMS to Your Applications
</h2>
<p className='text-muted-foreground mb-4 mx-auto max-w-3xl text-center'>
TextBee turns any Android phone into a reliable SMS gateway, allowing
you to send and receive text messages programmatically. Whether you're
building a notification system, implementing two-factor
authentication, or creating marketing campaigns, TextBee provides a
cost-effective solution without the need for complex telecom
integrations.
</p>
<p className='text-muted-foreground mx-auto max-w-3xl text-center'>
Follow this step-by-step guide to set up TextBee and start sending
your first SMS messages in minutes. Our straightforward process
requires minimal technical knowledge and works with any application or
service that can make HTTP requests.
</p>
</div>
<div className='space-y-12 mx-auto'>
<div id='step-1' className='pb-8 group'>
<div className='flex items-center gap-3 mb-6 justify-center'>
<div className='w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium shadow-sm'>
1
</div>
<h2 className='text-2xl font-semibold'>Account Setup</h2>
</div>
<div className='space-y-4'>
<p className='text-muted-foreground mb-4 text-center mx-auto max-w-3xl'>
Begin by creating your TextBee account and installing the Android
app. This setup process takes less than 2 minutes and only
requires basic permissions to send and receive SMS messages.
</p>
<div className='grid grid-cols-1 md:grid-cols-3 gap-4 max-w-4xl mx-auto'>
<div className='bg-card p-4 rounded-lg border hover:shadow-sm transition-shadow'>
<div className='flex justify-between mb-3'>
<span className='text-xl font-bold text-primary/80'>1</span>
<CheckCircle2 className='h-5 w-5 text-muted-foreground/50' />
</div>
<h3 className='font-medium mb-2'>Create account</h3>
<p className='text-sm text-muted-foreground'>
Register at{' '}
<a
href='https://textbee.dev'
className='text-primary hover:underline'
>
textbee.dev
</a>{' '}
with your email and password
</p>
</div>
<div className='bg-card p-4 rounded-lg border hover:shadow-sm transition-shadow'>
<div className='flex justify-between mb-3'>
<span className='text-xl font-bold text-primary/80'>2</span>
<Smartphone className='h-5 w-5 text-muted-foreground/50' />
</div>
<h3 className='font-medium mb-2'>Install app</h3>
<p className='text-sm text-muted-foreground'>
Download from{' '}
<a
href='https://dl.textbee.dev'
className='text-primary hover:underline'
>
dl.textbee.dev
</a>{' '}
or Google Play Store
</p>
</div>
<div className='bg-card p-4 rounded-lg border hover:shadow-sm transition-shadow'>
<div className='flex justify-between mb-3'>
<span className='text-xl font-bold text-primary/80'>3</span>
<Settings className='h-5 w-5 text-muted-foreground/50' />
</div>
<h3 className='font-medium mb-2'>Grant permissions</h3>
<p className='text-sm text-muted-foreground'>
Allow SMS access in the app to enable message sending and
receiving
</p>
</div>
</div>
</div>
</div>
<div id='step-2' className='pb-8 border-t pt-4 group'>
<div className='flex items-center gap-3 mb-6 justify-center'>
<div className='w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium shadow-sm'>
2
</div>
<h2 className='text-2xl font-semibold'>Connect Your Device</h2>
</div>
<div className='space-y-4'>
<p className='text-muted-foreground mb-4 text-center mx-auto max-w-3xl'>
Link your Android phone to your TextBee account to establish the
SMS gateway connection. This secure connection allows your
applications to send messages through your phone.
</p>
<div className='grid md:grid-cols-2 gap-6 max-w-4xl mx-auto'>
<div className='bg-card p-5 rounded-lg border hover:border-primary/30 transition-colors'>
<div className='flex items-center justify-between mb-4'>
<h3 className='font-medium'>QR Code Method</h3>
<span className='px-2 py-0.5 bg-primary/10 rounded text-xs text-primary'>
Recommended
</span>
</div>
<ol className='list-decimal ml-5 text-sm space-y-2'>
<li>Go to TextBee Dashboard</li>
<li>Click "Register Device"</li>
<li>Scan QR with app</li>
</ol>
</div>
<div className='bg-card p-5 rounded-lg border'>
<h3 className='font-medium mb-4'>Manual Method</h3>
<ol className='list-decimal ml-5 text-sm space-y-2'>
<li>Generate API key from dashboard</li>
<li>Open TextBee app</li>
<li>Enter the API key</li>
</ol>
</div>
</div>
</div>
</div>
<div id='step-3' className='pb-8 border-t pt-4 group'>
<div className='flex items-center gap-3 mb-6 justify-center'>
<div className='w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium shadow-sm'>
3
</div>
<h2 className='text-2xl font-semibold'>Send Your First SMS</h2>
</div>
<div className='space-y-4'>
<p className='text-muted-foreground mb-4 text-center mx-auto max-w-3xl'>
Start sending SMS messages through TextBee using either our
intuitive dashboard or direct API integration. Both methods
provide reliable message delivery with delivery status tracking.
</p>
<div className='grid md:grid-cols-2 gap-6 max-w-4xl mx-auto'>
<div className='bg-card p-5 rounded-lg border'>
<h3 className='font-medium mb-3 text-primary flex items-center gap-2'>
Dashboard Method
</h3>
<div className='bg-muted/50 p-4 rounded-md'>
<ol className='list-decimal ml-5 text-sm space-y-2'>
<li>Go to "Send SMS" section</li>
<li>Enter recipient(s)</li>
<li>Type your message</li>
<li>Click "Send"</li>
</ol>
</div>
</div>
<div className='bg-card p-5 rounded-lg border'>
<h3 className='font-medium mb-3 text-primary flex items-center gap-2'>
API Method
</h3>
<pre className='overflow-x-auto rounded-md bg-slate-950 p-3 text-xs'>
<code className='font-mono text-white'>
{`fetch("https://api.textbee.dev/api/v1/gateway/devices/{ID}/send-sms", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({
recipients: ['+1234567890'],
message: 'Hello!'
})
})`}
</code>
</pre>
</div>
</div>
<p className='text-sm text-muted-foreground text-center mx-auto max-w-3xl'>
With TextBee, your messages are sent directly through your Android
device, using your existing mobile plan. This keeps costs low
while maintaining high deliverability rates across all carriers.
</p>
</div>
</div>
<div id='step-4' className='pb-8 border-t pt-4 group'>
<div className='flex items-center gap-3 mb-6 justify-center'>
<div className='w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium shadow-sm'>
4
</div>
<h2 className='text-2xl font-semibold'>Receive SMS Messages</h2>
</div>
<div className='space-y-4'>
<p className='text-muted-foreground mb-4 text-center mx-auto max-w-3xl'>
Enable two-way communication by configuring TextBee to forward
incoming SMS messages to your application. This is essential for
interactive workflows, verification codes, and customer
engagement.
</p>
<div className='grid md:grid-cols-2 gap-6 max-w-4xl mx-auto'>
<div className='bg-card p-5 rounded-lg border'>
<h3 className='font-medium mb-3 text-primary'>Enable in App</h3>
<div className='bg-muted/50 p-4 rounded-md'>
<ol className='list-decimal ml-5 text-sm space-y-2'>
<li>Open the TextBee App</li>
<li>Go to Settings</li>
<li>Toggle "Receive SMS" on</li>
</ol>
</div>
</div>
<div className='bg-card p-5 rounded-lg border'>
<h3 className='font-medium mb-3 text-primary'>
Retrieve via API
</h3>
<pre className='overflow-x-auto rounded-md bg-slate-950 p-3 text-xs'>
<code className='font-mono text-white'>
{`fetch("https://api.textbee.dev/api/v1/gateway/devices/{ID}/get-received-sms", {
headers: { 'x-api-key': API_KEY }
})`}
</code>
</pre>
</div>
</div>
<p className='text-sm text-muted-foreground text-center mx-auto max-w-3xl'>
Received messages are securely forwarded to TextBee's servers and
can be accessed via the dashboard, API, or automatically sent to
your webhook endpoints for real-time processing.
</p>
</div>
</div>
<div id='step-5' className='pb-6 border-t pt-4 group'>
<div className='flex items-center gap-3 mb-6 justify-center'>
<div className='w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium shadow-sm'>
5
</div>
<h2 className='text-2xl font-semibold'>Advanced Features</h2>
</div>
<div className='space-y-4'>
<p className='text-muted-foreground mb-4 text-center mx-auto max-w-3xl'>
Once you've mastered the basics, explore TextBee's advanced
capabilities to enhance your SMS integration. These features help
scale your messaging operations and automate complex workflows.
</p>
<div className='grid sm:grid-cols-2 gap-3 mb-6 max-w-4xl mx-auto'>
<div className='flex items-start gap-2 p-3 bg-card rounded-md border'>
<div className='h-5 w-5 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs mt-0.5'>
</div>
<div>
<p className='font-medium'>Bulk SMS</p>
<p className='text-xs text-muted-foreground'>
Send to multiple recipients with a single API call for
efficient message broadcasting
</p>
</div>
</div>
<div className='flex items-start gap-2 p-3 bg-card rounded-md border'>
<div className='h-5 w-5 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs mt-0.5'>
</div>
<div>
<p className='font-medium'>Webhooks</p>
<p className='text-xs text-muted-foreground'>
Configure event-driven notifications for real-time updates
on message status
</p>
</div>
</div>
<div className='flex items-start gap-2 p-3 bg-card rounded-md border'>
<div className='h-5 w-5 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs mt-0.5'>
</div>
<div>
<p className='font-medium'>Multiple Devices</p>
<p className='text-xs text-muted-foreground'>
Connect several phones to increase throughput and add
redundancy
</p>
</div>
</div>
<div className='flex items-start gap-2 p-3 bg-card rounded-md border'>
<div className='h-5 w-5 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs mt-0.5'>
</div>
<div>
<p className='font-medium'>Self-hosting</p>
<p className='text-xs text-muted-foreground'>
Deploy TextBee on your own infrastructure for complete
control
</p>
</div>
</div>
</div>
<div className='bg-gradient-to-r from-primary/10 to-transparent p-4 rounded-md border border-primary/20 max-w-4xl mx-auto'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<BookOpen className='h-4 w-4 text-primary' />
<span className='font-medium'>Ready to explore more?</span>
</div>
<Link
href='/use-cases'
className='inline-flex items-center gap-1 px-3 py-1.5 rounded-md bg-primary/10 text-primary hover:bg-primary/20 transition-colors'
>
<span>Use Cases</span>
<ArrowRightCircle className='h-3 w-3' />
</Link>
</div>
</div>
</div>
</div>
</div>
<div
id='pro-plan'
className='my-16 bg-gradient-to-r from-primary/15 via-primary/10 to-transparent rounded-xl overflow-hidden border border-primary/20 shadow-sm max-w-4xl mx-auto'
>
<div className='p-1 bg-primary/20'></div>
<div className='px-6 py-8 md:px-8 text-center'>
<div className='flex items-center gap-2 mb-4 justify-center'>
<SparkleIcon className='h-5 w-5 text-primary' />
<h2 className='text-2xl font-bold'>TextBee Pro</h2>
</div>
<p className='text-lg mb-4 mx-auto max-w-2xl'>
Upgrade to TextBee Pro for enhanced features and priority support.
</p>
<div className='grid md:grid-cols-2 gap-6 mb-8 max-w-3xl mx-auto text-left'>
<div className='space-y-3'>
<div className='flex items-start gap-2'>
<CheckCircle2 className='h-5 w-5 text-primary shrink-0 mt-0.5' />
<span>Unlimited devices and higher sending limits</span>
</div>
<div className='flex items-start gap-2'>
<CheckCircle2 className='h-5 w-5 text-primary shrink-0 mt-0.5' />
<span>Advanced analytics and delivery reporting</span>
</div>
<div className='flex items-start gap-2'>
<CheckCircle2 className='h-5 w-5 text-primary shrink-0 mt-0.5' />
<span>Message scheduling and template management</span>
</div>
</div>
<div className='space-y-3'>
<div className='flex items-start gap-2'>
<CheckCircle2 className='h-5 w-5 text-primary shrink-0 mt-0.5' />
<span>Priority support with faster response times</span>
</div>
<div className='flex items-start gap-2'>
<CheckCircle2 className='h-5 w-5 text-primary shrink-0 mt-0.5' />
<span>Custom webhooks for advanced integrations</span>
</div>
<div className='flex items-start gap-2'>
<CheckCircle2 className='h-5 w-5 text-primary shrink-0 mt-0.5' />
<span>White-labeled SMS for business applications</span>
</div>
</div>
</div>
<div className='flex flex-col sm:flex-row sm:items-center gap-4 justify-center max-w-xl mx-auto'>
<div className='sm:flex-1 text-center sm:text-left'>
<div className='flex items-baseline gap-1 mb-1 justify-center sm:justify-start'>
<span className='text-3xl font-bold'>$29</span>
<span className='text-muted-foreground'>/month</span>
</div>
<p className='text-sm text-muted-foreground'>
Cancel anytime. No long-term contracts.
</p>
</div>
<a
href='https://textbee.dev/pricing'
className='px-6 py-3 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors font-medium text-center'
>
Upgrade to Pro
</a>
</div>
</div>
</div>
<div className='mb-16 border-t pt-8 max-w-4xl mx-auto'>
<h2 className='text-2xl font-semibold mb-4 text-center'>
Why Choose TextBee SMS Gateway?
</h2>
<div className='grid md:grid-cols-2 gap-8'>
<div>
<h3 className='text-lg font-medium mb-2 text-center'>
Cost-Effective SMS Solution
</h3>
<p className='text-muted-foreground text-sm mb-4 text-center'>
TextBee eliminates the need for expensive SMS API services or
telecom contracts. By using your existing phone and mobile plan,
you can send SMS messages at standard rates without additional
per-message fees from third-party providers.
</p>
<h3 className='text-lg font-medium mb-2 text-center'>
Easy Integration
</h3>
<p className='text-muted-foreground text-sm text-center'>
Our RESTful API makes integration simple for developers using any
programming language. TextBee works seamlessly with web
applications, mobile apps, and backend services through standard
HTTP requests.
</p>
</div>
<div>
<h3 className='text-lg font-medium mb-2 text-center'>
Perfect For
</h3>
<ul className='space-y-2 text-sm text-muted-foreground'>
<li className='flex items-start gap-2'>
<CheckCircle2 className='h-4 w-4 text-primary shrink-0 mt-0.5' />
<span>
<span className='font-medium'>
Two-factor authentication (2FA)
</span>{' '}
- Secure user accounts with SMS verification codes
</span>
</li>
<li className='flex items-start gap-2'>
<CheckCircle2 className='h-4 w-4 text-primary shrink-0 mt-0.5' />
<span>
<span className='font-medium'>Appointment reminders</span> -
Reduce no-shows with automated SMS notifications
</span>
</li>
<li className='flex items-start gap-2'>
<CheckCircle2 className='h-4 w-4 text-primary shrink-0 mt-0.5' />
<span>
<span className='font-medium'>Order updates</span> - Keep
customers informed about their purchases
</span>
</li>
<li className='flex items-start gap-2'>
<CheckCircle2 className='h-4 w-4 text-primary shrink-0 mt-0.5' />
<span>
<span className='font-medium'>Marketing campaigns</span> -
Engage customers with promotional messages
</span>
</li>
<li className='flex items-start gap-2'>
<CheckCircle2 className='h-4 w-4 text-primary shrink-0 mt-0.5' />
<span>
<span className='font-medium'>Alerts and notifications</span>{' '}
- Send time-sensitive information instantly
</span>
</li>
</ul>
</div>
</div>
</div>
<div className='mt-12 pt-6 border-t'>
<div className='max-w-lg mx-auto bg-card rounded-lg border p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4 text-center sm:text-left'>
<div className='flex flex-col items-center sm:items-start'>
<p className='font-medium mb-1'>Need help?</p>
<p className='text-xs text-muted-foreground'>
Our support team is ready to assist you
</p>
</div>
<div className='flex gap-3 justify-center'>
<a
href='mailto:contact@textbee.dev'
className='inline-flex items-center gap-1 px-3 py-1.5 rounded-md border hover:bg-muted transition-colors text-sm'
>
<ExternalLink className='h-3.5 w-3.5' />
<span>Email</span>
</a>
<a
href='https://docs.textbee.dev'
className='inline-flex items-center gap-1 px-3 py-1.5 rounded-md border hover:bg-muted transition-colors text-sm'
>
<BookOpen className='h-3.5 w-3.5' />
<span>Docs</span>
</a>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,161 +0,0 @@
import { Metadata } from 'next'
import { Card, CardContent } from '@/components/ui/card'
export const metadata: Metadata = {
title: 'Refund Policy | TextBee',
description: 'Refund Policy for TextBee SMS Gateway Platform',
}
export default function RefundPolicyPage() {
return (
<div className='container max-w-7xl py-6 md:px-12'>
<Card className='border-none shadow-none'>
<CardContent className='space-y-6'>
<h1 className='scroll-m-20 text-4xl font-bold tracking-tight lg:text-5xl'>
Refund Policy
</h1>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
Effective Date: February 15, 2025
</h2>
<p className='leading-7 [&:not(:first-child)]:mt-6'>
Thank you for choosing TextBee SMS Gateway Platform. This Refund Policy outlines our procedures and guidelines regarding refunds for our services. By using our Platform, you agree to the terms of this Refund Policy.
</p>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
1. Subscription Services
</h2>
<p className='leading-7'>
TextBee offers both free and paid subscription plans for our SMS Gateway services. Our refund policy for paid subscription services is as follows:
</p>
<h3 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
1.1 Free Tier:
</h3>
<p className='leading-7'>
Our free tier is available at no cost and therefore does not qualify for refunds. Users can downgrade to the free tier at any time.
</p>
<h3 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
1.2 Monthly Subscriptions:
</h3>
<p className='leading-7'>
For monthly subscription plans, we offer a 7-day money-back guarantee from the date of purchase. If you are not satisfied with our services, you may request a full refund within this period. After the 7-day period, no refunds will be provided for the current billing cycle.
</p>
<h3 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
1.3 Annual Subscriptions:
</h3>
<p className='leading-7'>
For annual subscription plans, we offer a 14-day money-back guarantee from the date of purchase. If you are not satisfied with our services, you may request a full refund within this period. After the 14-day period, we may provide a prorated refund for the unused portion of your subscription at our discretion.
</p>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
2. Future Usage-Based Billing
</h2>
<p className='leading-7'>
For our planned usage-based billing options:
</p>
<ul className='my-6 ml-6 list-disc [&>li]:mt-2'>
<li>Unused credits may be eligible for a refund within 30 days of purchase.</li>
<li>Once credits have been used, they are not eligible for a refund.</li>
<li>Custom implementation services or integration assistance fees are non-refundable once the work has commenced.</li>
</ul>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
3. How to Request a Refund
</h2>
<p className='leading-7'>
To request a refund, please contact our customer support team at{' '}
<a
href='mailto:support@textbee.dev'
className='font-medium text-primary underline underline-offset-4 hover:text-primary/80'
>
support@textbee.dev
</a>{' '}
with the following information:
</p>
<ul className='my-6 ml-6 list-disc [&>li]:mt-2'>
<li>Your account email address</li>
<li>Date of purchase</li>
<li>Reason for requesting a refund</li>
<li>Order or transaction ID (if available)</li>
</ul>
<p className='leading-7'>
We will process your refund request within 5-7 business days and notify you of the outcome.
</p>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
4. Exceptions
</h2>
<p className='leading-7'>
We reserve the right to deny refund requests in the following cases:
</p>
<ul className='my-6 ml-6 list-disc [&>li]:mt-2'>
<li>Violation of our Terms of Service</li>
<li>Fraudulent or abusive use of our services</li>
<li>Requests made after the eligible refund period</li>
<li>Services that have been fully delivered or consumed</li>
</ul>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
5. Service Interruption or Failure
</h2>
<p className='leading-7'>
In the event of significant service interruption or failure to provide the services you have paid for, you may be eligible for a refund regardless of the standard refund periods outlined above. Such cases include:
</p>
<ul className='my-6 ml-6 list-disc [&>li]:mt-2'>
<li>Extended platform outages (exceeding 24 hours)</li>
<li>Failure to deliver core SMS gateway functionality</li>
<li>Significant degradation of service that prevents normal business operations</li>
</ul>
<p className='leading-7'>
Please contact our support team with details of the service interruption or failure, and we will assess your refund eligibility on a case-by-case basis. In some instances, we may offer service credits or partial refunds proportional to the duration and severity of the service issue.
</p>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
6. Refund Methods
</h2>
<p className='leading-7'>
Refunds will be issued using the same payment method used for the original purchase. Processing times may vary depending on your payment provider.
</p>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
7. Changes to This Policy
</h2>
<p className='leading-7'>
We may update this Refund Policy from time to time. We will notify you of any changes by posting the new Refund Policy on this page and updating the "Effective Date" at the top of this policy.
</p>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
8. Contact Us
</h2>
<p className='leading-7'>
If you have any questions or concerns about this Refund Policy, please contact us at{' '}
<a
href='mailto:contact@textbee.dev'
className='font-medium text-primary underline underline-offset-4 hover:text-primary/80'
>
contact@textbee.dev
</a>
.
</p>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -1,139 +0,0 @@
import { Metadata } from 'next'
import { Card, CardContent } from '@/components/ui/card'
export const metadata: Metadata = {
title: 'Terms of Service | TextBee',
description: 'Terms of Service for TextBee SMS Gateway Platform',
}
export default function TermsOfServicePage() {
return (
<div className='container max-w-7xl py-6 md:px-12'>
<Card className='border-none shadow-none'>
<CardContent className='space-y-6'>
<h1 className='scroll-m-20 text-4xl font-bold tracking-tight lg:text-5xl'>
Terms of Service
</h1>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
Effective Date: January 2024
</h2>
<p className='leading-7 [&:not(:first-child)]:mt-6'>
Welcome to TextBee SMS Gateway Platform. These Terms of Service (&ldquo;Terms&rdquo;) govern your access to and use of our services, including our website, mobile applications, APIs, and other software (&ldquo;Services&rdquo;). By accessing or using our Services, you agree to be bound by these Terms.
</p>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
MIT License
</h2>
<p className='leading-7'>
Copyright (c) 2024 TextBee
</p>
<p className='leading-7'>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the &ldquo;Software&rdquo;), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
</p>
<p className='leading-7'>
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
</p>
<p className='leading-7'>
THE SOFTWARE IS PROVIDED &ldquo;AS IS&rdquo;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</p>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
1. Description of Services
</h2>
<p className='leading-7'>
TextBee provides an SMS gateway platform that allows users to send and receive SMS messages through their Android devices. Our Services include:
</p>
<ul className='my-6 ml-6 list-disc [&>li]:mt-2'>
<li>SMS gateway functionality</li>
<li>API access for integration with other applications</li>
<li>Web dashboard for managing SMS communications</li>
<li>Analytics and reporting tools</li>
</ul>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
2. User Responsibilities
</h2>
<p className='leading-7'>
While our software is provided under the MIT license, users are still responsible for:
</p>
<ul className='my-6 ml-6 list-disc [&>li]:mt-2'>
<li>Complying with all applicable laws and regulations, including those related to SMS messaging, spam, and data privacy</li>
<li>Obtaining proper consent from recipients before sending SMS messages</li>
<li>Not using our Services for any illegal, harmful, or fraudulent activities</li>
<li>Maintaining the security of their account credentials</li>
</ul>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
3. Privacy
</h2>
<p className='leading-7'>
Our Privacy Policy, available at{' '}
<a
href='/privacy-policy'
className='font-medium text-primary underline underline-offset-4 hover:text-primary/80'
>
Privacy Policy
</a>
, describes how we collect, use, and share your personal information. By using our Services, you consent to our collection and use of your information as described in the Privacy Policy.
</p>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
4. Disclaimer of Warranty
</h2>
<p className='leading-7'>
As stated in the MIT license, the software is provided &ldquo;as is&rdquo;, without warranty of any kind. We make no guarantees regarding the reliability, availability, or suitability of our Services for your specific needs.
</p>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
5. Changes to Terms
</h2>
<p className='leading-7'>
We may update these Terms from time to time. We will notify you of any changes by posting the new Terms on this page and updating the "Effective Date" at the top of these Terms. Your continued use of our Services after such changes constitutes your acceptance of the new Terms.
</p>
</div>
<div className='space-y-4'>
<h2 className='scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight'>
6. Contact Us
</h2>
<p className='leading-7'>
If you have any questions or concerns about these Terms, please contact us at{' '}
<a
href='mailto:contact@textbee.dev'
className='font-medium text-primary underline underline-offset-4 hover:text-primary/80'
>
contact@textbee.dev
</a>
.
</p>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -1,630 +0,0 @@
import { Metadata } from 'next'
import { Card, CardContent } from '@/components/ui/card'
import Link from 'next/link'
import {
ShieldCheck,
ShoppingBag,
Calendar,
AlertTriangle,
Megaphone,
HeadsetIcon,
ExternalLink,
ArrowRight,
} from 'lucide-react'
export const metadata: Metadata = {
title: 'Use Cases | TextBee',
description:
'Explore various use cases and applications for TextBee SMS Gateway Platform',
}
export default function UseCasesPage() {
return (
<>
<div className='container max-w-7xl mx-auto py-10 px-4 md:px-12'>
<div className='rounded-lg bg-gradient-to-r from-primary/20 via-primary/10 to-background p-8 mb-12 mx-auto'>
<h1 className='scroll-m-20 text-4xl font-bold tracking-tight lg:text-5xl mb-4 text-center'>
TextBee Use Cases
</h1>
<p className='text-xl leading-relaxed max-w-3xl mx-auto text-center'>
Discover how businesses and developers leverage TextBee SMS Gateway
for a wide variety of applications. Get inspired by these common use
cases and implementations.
</p>
</div>
<Card className='border-none shadow-none mx-auto'>
<CardContent className='space-y-10 px-0'>
<div className='grid gap-8 md:grid-cols-2 mx-auto'>
<div className='rounded-xl border bg-card p-6 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden'>
<div className='absolute top-0 right-0 w-24 h-24 bg-primary/5 rounded-full -mt-8 -mr-8'></div>
<div className='flex items-center gap-4 mb-5'>
<div className='w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center'>
<ShieldCheck className='h-6 w-6 text-primary' />
</div>
<h2 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
Two-factor Authentication (2FA)
</h2>
</div>
<p className='leading-7 mb-5 text-muted-foreground'>
Enhance your application's security by implementing SMS-based
two-factor authentication. Add an extra layer of verification
to protect user accounts.
</p>
<div className='bg-muted p-4 rounded-lg mb-5'>
<h3 className='font-medium text-base mb-2'>
Implementation Steps:
</h3>
<ol className='ml-6 list-decimal space-y-1'>
<li>Generate a random verification code for the user</li>
<li>Send the code via SMS using TextBee API</li>
<li>Verify the code entered by the user</li>
</ol>
</div>
<div className='relative'>
<div className='flex items-center gap-1 text-xs text-muted-foreground mb-2'>
<span className='px-2 py-0.5 bg-primary/10 rounded text-primary font-medium'>
JavaScript
</span>
<span className='text-muted-foreground'>Example:</span>
</div>
<pre className='overflow-x-auto rounded-lg bg-slate-950 p-4 text-xs'>
<code className='font-mono text-white'>
{`// Send 2FA code
const verificationCode = generateRandomCode();
await axios.post(\`https://api.textbee.dev/api/v1/gateway/devices/\${DEVICE_ID}/send-sms\`, {
recipients: [ user.phoneNumber ],
message: \`Your verification code is: \${verificationCode}\`
}, {
headers: { 'x-api-key': API_KEY }
});`}
</code>
</pre>
<button className='absolute top-3 right-3 p-1.5 rounded-md bg-slate-800 hover:bg-slate-700 transition-colors'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='text-slate-300'
>
<rect width='14' height='14' x='8' y='8' rx='2' ry='2' />
<path d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2' />
</svg>
</button>
</div>
</div>
<div className='rounded-xl border bg-card p-6 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden'>
<div className='absolute top-0 right-0 w-24 h-24 bg-primary/5 rounded-full -mt-8 -mr-8'></div>
<div className='flex items-center gap-4 mb-5'>
<div className='w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center'>
<ShoppingBag className='h-6 w-6 text-primary' />
</div>
<h2 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
Order Notifications
</h2>
</div>
<p className='leading-7 mb-5 text-muted-foreground'>
Keep customers informed about their orders with real-time SMS
updates. Improve customer experience with timely notifications
throughout the order lifecycle.
</p>
<div className='grid grid-cols-2 gap-2 mb-5'>
<div className='border rounded-lg p-3 bg-background'>
<h3 className='text-sm font-medium mb-1'>
Order Confirmation
</h3>
<p className='text-xs text-muted-foreground'>
Send details after purchase
</p>
</div>
<div className='border rounded-lg p-3 bg-background'>
<h3 className='text-sm font-medium mb-1'>
Shipping Updates
</h3>
<p className='text-xs text-muted-foreground'>
Notify when order ships
</p>
</div>
<div className='border rounded-lg p-3 bg-background'>
<h3 className='text-sm font-medium mb-1'>
Delivery Status
</h3>
<p className='text-xs text-muted-foreground'>
Alert when delivered
</p>
</div>
<div className='border rounded-lg p-3 bg-background'>
<h3 className='text-sm font-medium mb-1'>Order Changes</h3>
<p className='text-xs text-muted-foreground'>
Inform of modifications
</p>
</div>
</div>
<div className='relative'>
<div className='flex items-center gap-1 text-xs text-muted-foreground mb-2'>
<span className='px-2 py-0.5 bg-primary/10 rounded text-primary font-medium'>
JavaScript
</span>
<span className='text-muted-foreground'>Example:</span>
</div>
<pre className='overflow-x-auto rounded-lg bg-slate-950 p-4 text-xs'>
<code className='font-mono text-white'>
{`// Send order confirmation
await axios.post(\`https://api.textbee.dev/api/v1/gateway/devices/\${DEVICE_ID}/send-sms\`, {
recipients: [ customer.phoneNumber ],
message: \`Order #\${orderNumber} confirmed! Expected delivery: \${deliveryDate}. Track at: \${trackingUrl}\`
}, {
headers: { 'x-api-key': API_KEY }
});`}
</code>
</pre>
<button className='absolute top-3 right-3 p-1.5 rounded-md bg-slate-800 hover:bg-slate-700 transition-colors'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='text-slate-300'
>
<rect width='14' height='14' x='8' y='8' rx='2' ry='2' />
<path d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2' />
</svg>
</button>
</div>
</div>
<div className='rounded-xl border bg-card p-6 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden'>
<div className='absolute top-0 right-0 w-24 h-24 bg-primary/5 rounded-full -mt-8 -mr-8'></div>
<div className='flex items-center gap-4 mb-5'>
<div className='w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center'>
<Calendar className='h-6 w-6 text-primary' />
</div>
<h2 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
Appointment Reminders
</h2>
</div>
<p className='leading-7 mb-5 text-muted-foreground'>
Reduce no-shows by sending automated appointment reminders to
clients. Perfect for medical practices, salons, consultants,
and service businesses.
</p>
<div className='bg-gradient-to-r from-primary/10 to-background p-4 rounded-lg mb-5'>
<h3 className='font-medium text-base mb-2'>Key Features:</h3>
<ul className='ml-6 list-disc space-y-1'>
<li>Scheduled reminders (24h, 1h before appointments)</li>
<li>Interactive responses (reply to reschedule/cancel)</li>
<li>Calendar integration</li>
</ul>
</div>
<div className='relative'>
<div className='flex items-center gap-1 text-xs text-muted-foreground mb-2'>
<span className='px-2 py-0.5 bg-primary/10 rounded text-primary font-medium'>
JavaScript
</span>
<span className='text-muted-foreground'>Example:</span>
</div>
<pre className='overflow-x-auto rounded-lg bg-slate-950 p-4 text-xs'>
<code className='font-mono text-white'>
{`// Schedule reminder job
scheduler.scheduleJob(reminderTime, async () => {
await axios.post(\`https://api.textbee.dev/api/v1/gateway/devices/\${DEVICE_ID}/send-sms\`, {
recipients: [ appointment.phoneNumber ],
message: \`Reminder: Your appointment is tomorrow at \${appointment.time}. Reply CONFIRM to confirm or RESCHEDULE to change.\`
}, {
headers: { 'x-api-key': API_KEY }
});
});`}
</code>
</pre>
<button className='absolute top-3 right-3 p-1.5 rounded-md bg-slate-800 hover:bg-slate-700 transition-colors'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='text-slate-300'
>
<rect width='14' height='14' x='8' y='8' rx='2' ry='2' />
<path d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2' />
</svg>
</button>
</div>
</div>
<div className='rounded-xl border bg-card p-6 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden'>
<div className='absolute top-0 right-0 w-24 h-24 bg-primary/5 rounded-full -mt-8 -mr-8'></div>
<div className='flex items-center gap-4 mb-5'>
<div className='w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center'>
<AlertTriangle className='h-6 w-6 text-primary' />
</div>
<h2 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
Emergency Alerts
</h2>
</div>
<p className='leading-7 mb-5 text-muted-foreground'>
Send critical notifications and emergency alerts to large
groups of people quickly. Perfect for natural disasters,
emergencies, and critical business communications.
</p>
<div className='bg-gradient-to-r from-primary/10 to-background p-4 rounded-lg mb-5'>
<h3 className='font-medium text-base mb-2'>Applications:</h3>
<ul className='ml-6 list-disc space-y-1'>
<li>Weather emergencies</li>
<li>Campus/school alerts</li>
<li>IT system outages</li>
<li>Critical business communications</li>
</ul>
</div>
<div className='relative'>
<div className='flex items-center gap-1 text-xs text-muted-foreground mb-2'>
<span className='px-2 py-0.5 bg-primary/10 rounded text-primary font-medium'>
JavaScript
</span>
<span className='text-muted-foreground'>Example:</span>
</div>
<pre className='overflow-x-auto rounded-lg bg-slate-950 p-4 text-xs'>
<code className='font-mono text-white'>
{`// Send bulk emergency alert
const recipients = await getUserPhoneNumbers(affectedRegion);
await axios.post(\`https://api.textbee.dev/api/v1/gateway/devices/\${DEVICE_ID}/send-bulk-sms\`, {
messageTemplate: \`ALERT: \${emergencyMessage}. Stay safe.\`,
messages: [{
recipients: recipients,
}]
}, {
headers: { 'x-api-key': API_KEY }
});`}
</code>
</pre>
<button className='absolute top-3 right-3 p-1.5 rounded-md bg-slate-800 hover:bg-slate-700 transition-colors'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='text-slate-300'
>
<rect width='14' height='14' x='8' y='8' rx='2' ry='2' />
<path d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2' />
</svg>
</button>
</div>
</div>
<div className='rounded-xl border bg-card p-6 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden'>
<div className='absolute top-0 right-0 w-24 h-24 bg-primary/5 rounded-full -mt-8 -mr-8'></div>
<div className='flex items-center gap-4 mb-5'>
<div className='w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center'>
<Megaphone className='h-6 w-6 text-primary' />
</div>
<h2 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
Marketing Campaigns
</h2>
</div>
<p className='leading-7 mb-5 text-muted-foreground'>
Run targeted SMS marketing campaigns to engage customers and
drive sales. Perfect for promotions, event announcements, and
customer surveys.
</p>
<div className='bg-gradient-to-r from-primary/10 to-background p-4 rounded-lg mb-5'>
<h3 className='font-medium text-base mb-2'>
Campaign Types:
</h3>
<ul className='ml-6 list-disc space-y-1'>
<li>Promotional offers and discounts</li>
<li>New product announcements</li>
<li>Event invitations</li>
<li>Customer surveys</li>
</ul>
</div>
<div className='bg-amber-50 dark:bg-amber-950 p-4 rounded-lg my-4'>
<p className='text-amber-800 dark:text-amber-200 text-sm'>
<strong>Note:</strong> Always ensure you have proper consent
and comply with SMS marketing regulations in your region.
</p>
</div>
</div>
<div className='rounded-xl border bg-card p-6 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden'>
<div className='absolute top-0 right-0 w-24 h-24 bg-primary/5 rounded-full -mt-8 -mr-8'></div>
<div className='flex items-center gap-4 mb-5'>
<div className='w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center'>
<HeadsetIcon className='h-6 w-6 text-primary' />
</div>
<h2 className='scroll-m-20 text-2xl font-semibold tracking-tight'>
Customer Support
</h2>
</div>
<p className='leading-7 mb-5 text-muted-foreground'>
Provide customer support through two-way SMS communication.
Perfect for handling customer inquiries and feedback.
</p>
<div className='bg-gradient-to-r from-primary/10 to-background p-4 rounded-lg mb-5'>
<h3 className='font-medium text-base mb-2'>
Implementation Steps:
</h3>
<ol className='ml-6 list-decimal space-y-1'>
<li>Configure webhook for incoming SMS</li>
<li>Process and route messages to support agents</li>
<li>Send automated responses for common queries</li>
<li>Track conversation history</li>
</ol>
</div>
<div className='relative'>
<div className='flex items-center gap-1 text-xs text-muted-foreground mb-2'>
<span className='px-2 py-0.5 bg-primary/10 rounded text-primary font-medium'>
JavaScript
</span>
<span className='text-muted-foreground'>Example:</span>
</div>
<pre className='overflow-x-auto rounded-lg bg-slate-950 p-4 text-xs'>
<code className='font-mono text-white'>
{`// Check for new messages
const messages = await axios.get(
\`https://api.textbee.dev/api/v1/gateway/devices/\${DEVICE_ID}/get-received-sms\`,
{ headers: { 'x-api-key': API_KEY } }
);
// Process and respond to messages
for (const msg of messages.data) {
const response = await generateSupportResponse(msg.message);
await sendReply(msg.sender, response);
}`}
</code>
</pre>
<button className='absolute top-3 right-3 p-1.5 rounded-md bg-slate-800 hover:bg-slate-700 transition-colors'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='text-slate-300'
>
<rect width='14' height='14' x='8' y='8' rx='2' ry='2' />
<path d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2' />
</svg>
</button>
</div>
</div>
</div>
<div className='mt-16 space-y-6 mx-auto'>
<div className='flex items-center gap-3 mb-6 justify-center'>
<div className='w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center'>
<ExternalLink className='h-4 w-4 text-primary' />
</div>
<h2 className='scroll-m-20 text-3xl font-semibold tracking-tight'>
Custom Integrations
</h2>
</div>
<p className='leading-7 text-muted-foreground max-w-3xl mx-auto text-center'>
TextBee can be integrated with various platforms and services.
Our REST API allows you to create custom integrations for almost
any application, automating SMS sending and receiving based on
triggers in your existing systems.
</p>
<div className='grid grid-cols-2 md:grid-cols-4 gap-4 my-8 mx-auto'>
<div className='p-6 border rounded-xl text-center hover:border-primary/50 hover:shadow-md transition-all bg-card'>
<div className='w-12 h-12 mx-auto mb-3 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center'>
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-6 w-6 text-blue-700 dark:text-blue-300'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2' />
<circle cx='9' cy='7' r='4' />
<path d='M22 21v-2a4 4 0 0 0-3-3.87' />
<path d='M16 3.13a4 4 0 0 1 0 7.75' />
</svg>
</div>
<p className='font-medium text-lg'>CRM Systems</p>
<p className='text-sm text-muted-foreground mt-2'>
Connect SMS messaging with customer records
</p>
</div>
<div className='p-6 border rounded-xl text-center hover:border-primary/50 hover:shadow-md transition-all bg-card'>
<div className='w-12 h-12 mx-auto mb-3 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center'>
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-6 w-6 text-purple-700 dark:text-purple-300'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<rect width='18' height='18' x='3' y='4' rx='2' ry='2' />
<line x1='16' x2='16' y1='2' y2='6' />
<line x1='8' x2='8' y1='2' y2='6' />
<line x1='3' x2='21' y1='10' y2='10' />
<path d='M8 14h.01' />
<path d='M12 14h.01' />
<path d='M16 14h.01' />
<path d='M8 18h.01' />
<path d='M12 18h.01' />
<path d='M16 18h.01' />
</svg>
</div>
<p className='font-medium text-lg'>Booking Software</p>
<p className='text-sm text-muted-foreground mt-2'>
Automate appointment confirmations
</p>
</div>
<div className='p-6 border rounded-xl text-center hover:border-primary/50 hover:shadow-md transition-all bg-card'>
<div className='w-12 h-12 mx-auto mb-3 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center'>
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-6 w-6 text-green-700 dark:text-green-300'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<circle cx='8' cy='21' r='1' />
<circle cx='19' cy='21' r='1' />
<path d='M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12' />
</svg>
</div>
<p className='font-medium text-lg'>E-commerce</p>
<p className='text-sm text-muted-foreground mt-2'>
Send order & shipping updates
</p>
</div>
<div className='p-6 border rounded-xl text-center hover:border-primary/50 hover:shadow-md transition-all bg-card'>
<div className='w-12 h-12 mx-auto mb-3 rounded-full bg-amber-100 dark:bg-amber-900 flex items-center justify-center'>
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-6 w-6 text-amber-700 dark:text-amber-300'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<rect width='18' height='18' x='3' y='3' rx='2' />
<path d='M12 8v8' />
<path d='m8.5 14 7-4' />
<path d='m8.5 10 7 4' />
</svg>
</div>
<p className='font-medium text-lg'>Automation Tools</p>
<p className='text-sm text-muted-foreground mt-2'>
Integrate with Zapier, IFTTT, etc.
</p>
</div>
</div>
<div className='mt-6 grid md:grid-cols-2 gap-8 mx-auto'>
<div className='bg-muted p-6 rounded-xl'>
<h3 className='text-xl font-medium mb-3'>Webhooks Support</h3>
<p className='text-muted-foreground mb-4'>
Configure webhooks to receive notifications when SMS events
occur. Perfect for event-driven architectures and real-time
applications.
</p>
<div className='text-xs p-2 bg-slate-200 dark:bg-slate-800 rounded-lg font-mono overflow-x-auto'>
POST
https://your-server.com/webhook?event=sms_received&sender=+1234567890
</div>
</div>
<div className='bg-muted p-6 rounded-xl'>
<h3 className='text-xl font-medium mb-3'>
API Documentation
</h3>
<p className='text-muted-foreground mb-4'>
Our comprehensive API documentation provides all the details
you need to integrate TextBee with your applications and
services.
</p>
<a
href='https://docs.textbee.dev'
className='inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors text-sm'
>
<span>View API Documentation</span>
<ArrowRight className='h-3.5 w-3.5' />
</a>
</div>
</div>
</div>
<div className='p-8 bg-gradient-to-r from-primary/20 via-primary/10 to-primary/5 rounded-xl mt-12 mx-auto'>
<div className='flex flex-col md:flex-row md:items-center justify-between gap-6'>
<div>
<h2 className='scroll-m-20 text-2xl font-semibold tracking-tight mb-2'>
Ready to implement these use cases?
</h2>
<p className='leading-7 text-muted-foreground max-w-2xl'>
Follow our step-by-step quickstart guide to set up TextBee
and start sending SMS messages in minutes. Whether you're
implementing 2FA, appointment reminders, or complex
integrations, we've got you covered.
</p>
</div>
<div className='flex flex-col sm:flex-row gap-4'>
<Link
href='/quickstart'
className='inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors'
>
<span>Quickstart Guide</span>
<ArrowRight className='h-4 w-4' />
</Link>
<a
href='mailto:contact@textbee.dev'
className='inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg bg-background border hover:bg-muted transition-colors'
>
<span>Contact Support</span>
</a>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</>
)
}

536
web/app/download/page.tsx Normal file
View File

@@ -0,0 +1,536 @@
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import {
Download,
Clock,
Calendar,
ArrowDownToLine,
FileDown,
Tag,
Github,
PackageOpen,
Info,
ChevronDown,
Check,
ExternalLink,
} from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
import { Skeleton } from '@/components/ui/skeleton'
interface Release {
id: number
name: string
tag_name: string
published_at: string
body: string
html_url: string
assets: Array<{
id: number
name: string
browser_download_url: string
size: number
download_count: number
}>
prerelease: boolean
}
export default function DownloadPage() {
const [releases, setReleases] = useState<Release[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
async function fetchReleases() {
try {
const response = await fetch(
'https://api.github.com/repos/vernu/textbee/releases'
)
if (!response.ok) {
throw new Error('Failed to fetch releases')
}
const data = await response.json()
setReleases(data)
} catch (err) {
setError('Failed to load releases. Please try again later.')
console.error(err)
} finally {
setLoading(false)
}
}
fetchReleases()
}, [])
// Get the latest stable release (not prerelease)
const latestRelease = releases.find((release) => !release.prerelease)
// Format date
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
// Format file size
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + ' B'
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
else return (bytes / 1048576).toFixed(1) + ' MB'
}
// Parse markdown lists from release notes
const parseReleaseNotes = (body: string) => {
if (!body) return { description: '', changelog: [] }
const lines = body.split('\n').map((line) => line.trim())
// Find where the changelog starts (usually after ## Changelog or similar)
const changelogIndex = lines.findIndex(
(line) =>
line.startsWith('##') &&
(line.toLowerCase().includes('changelog') ||
line.toLowerCase().includes('changes') ||
line.toLowerCase().includes("what's new"))
)
let description = ''
let changelog: string[] = []
if (changelogIndex > 0) {
description = lines.slice(0, changelogIndex).join('\n')
changelog = lines
.slice(changelogIndex + 1)
.filter((line) => line.startsWith('-') || line.startsWith('*'))
.map((line) => line.substring(1).trim())
} else {
// If no explicit changelog section, treat list items as changelog
const listItems = lines.filter(
(line) => line.startsWith('-') || line.startsWith('*')
)
if (listItems.length > 0) {
changelog = listItems.map((line) => line.substring(1).trim())
description = lines
.filter((line) => !line.startsWith('-') && !line.startsWith('*'))
.join('\n')
} else {
description = body
}
}
return { description, changelog }
}
return (
<div className='min-h-screen py-16 px-4'>
<div className='container mx-auto max-w-5xl'>
<div className='text-center mb-12'>
<div className='inline-flex items-center rounded-full border px-3 py-1 text-sm bg-brand-50 dark:bg-brand-950 border-brand-200 dark:border-brand-800 text-brand-700 dark:text-brand-300 mb-4'>
<Download className='h-3.5 w-3.5 mr-2' /> Download TextBee
</div>
<h1 className='text-4xl font-bold tracking-tight text-gray-900 dark:text-white'>
Download TextBee App
</h1>
<p className='mt-4 text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto'>
Transform your Android device into a powerful SMS gateway with our
easy-to-use application.
</p>
</div>
{/* Latest release section */}
<div className='mb-16'>
<div className='bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden'>
<div className='p-6 sm:p-8'>
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6'>
<div>
<div className='flex items-center gap-2 mb-2'>
<Badge className='bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300 hover:bg-green-100'>
Latest Version
</Badge>
{latestRelease?.prerelease && (
<Badge
variant='outline'
className='bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300'
>
Beta
</Badge>
)}
</div>
{loading ? (
<Skeleton className='h-8 w-48' />
) : error ? (
<h2 className='text-2xl font-bold text-gray-900 dark:text-white'>
TextBee App
</h2>
) : (
<h2 className='text-2xl font-bold text-gray-900 dark:text-white'>
{latestRelease?.name || 'TextBee App'}
</h2>
)}
</div>
{loading ? (
<Skeleton className='h-10 w-36' />
) : error ? (
<Button disabled>Download Unavailable</Button>
) : latestRelease?.assets?.length ? (
<Button
size='lg'
className='bg-brand-600 hover:bg-brand-700 text-white'
asChild
>
<Link
href={latestRelease.assets[0].browser_download_url}
target='_blank'
rel='noopener noreferrer'
>
<ArrowDownToLine className='mr-2 h-5 w-5' />
Download Now
</Link>
</Button>
) : (
<Button disabled>No Downloads Available</Button>
)}
</div>
{loading ? (
<div className='space-y-4'>
<Skeleton className='h-4 w-full' />
<Skeleton className='h-4 w-full' />
<Skeleton className='h-4 w-3/4' />
</div>
) : error ? (
<div className='text-red-500 dark:text-red-400'>{error}</div>
) : latestRelease ? (
<>
<div className='flex flex-wrap gap-4 mb-6 text-sm text-gray-600 dark:text-gray-400'>
<div className='flex items-center'>
<Tag className='h-4 w-4 mr-1' />
<span>Version: {latestRelease.tag_name}</span>
</div>
<div className='flex items-center'>
<Calendar className='h-4 w-4 mr-1' />
<span>
Released: {formatDate(latestRelease.published_at)}
</span>
</div>
{latestRelease.assets?.[0] && (
<>
<div className='flex items-center'>
<FileDown className='h-4 w-4 mr-1' />
<span>
Size: {formatFileSize(latestRelease.assets[0].size)}
</span>
</div>
<div className='flex items-center'>
<Download className='h-4 w-4 mr-1' />
<span>
Downloads:{' '}
{latestRelease.assets[0].download_count.toLocaleString()}
</span>
</div>
</>
)}
</div>
{/* Release details */}
<div className='space-y-4'>
{(() => {
const { description, changelog } = parseReleaseNotes(
latestRelease.body || ''
)
return (
<>
{description && (
<div className='text-gray-700 dark:text-gray-300'>
{description.split('\n').map((line, i) => (
<p key={i} className='mb-2'>
{line}
</p>
))}
</div>
)}
{changelog.length > 0 && (
<div>
<h3 className='text-lg font-semibold mb-2 text-gray-900 dark:text-white'>
What's New:
</h3>
<ul className='space-y-1 list-disc pl-5 text-gray-600 dark:text-gray-400'>
{changelog.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
</div>
)}
</>
)
})()}
</div>
<div className='mt-6 pt-6 border-t border-gray-200 dark:border-gray-700'>
<div className='flex flex-col sm:flex-row sm:items-center gap-4'>
<Button variant='outline' size='sm' asChild>
<Link
href={latestRelease.html_url}
target='_blank'
rel='noopener noreferrer'
>
<Github className='mr-2 h-4 w-4' />
View on GitHub
</Link>
</Button>
<div className='text-sm text-gray-500 dark:text-gray-400'>
Compatible with Android 7.0+ devices.
</div>
</div>
</div>
</>
) : (
<div className='text-gray-600 dark:text-gray-400'>
No releases available at this time.
</div>
)}
</div>
</div>
</div>
{/* All releases section */}
<div>
<div className='flex items-center justify-between mb-6'>
<h2 className='text-2xl font-bold text-gray-900 dark:text-white'>
All Releases
</h2>
<Button
variant='outline'
size='sm'
asChild
className='text-gray-600 dark:text-gray-400'
>
<Link
href='https://github.com/vernu/textbee/releases'
target='_blank'
rel='noopener noreferrer'
>
<ExternalLink className='mr-2 h-4 w-4' />
View All on GitHub
</Link>
</Button>
</div>
{loading ? (
<div className='space-y-4'>
{[1, 2, 3].map((i) => (
<div
key={i}
className='bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4'
>
<Skeleton className='h-6 w-48 mb-4' />
<Skeleton className='h-4 w-full mb-2' />
<Skeleton className='h-4 w-3/4' />
</div>
))}
</div>
) : error ? (
<div className='bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6'>
<div className='text-red-500 dark:text-red-400'>{error}</div>
</div>
) : releases.length === 0 ? (
<div className='bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6 text-center'>
<PackageOpen className='h-12 w-12 mx-auto text-gray-400 mb-4' />
<h3 className='text-lg font-medium text-gray-900 dark:text-white mb-2'>
No Releases Found
</h3>
<p className='text-gray-600 dark:text-gray-400'>
There are no releases available at this time.
</p>
</div>
) : (
<Accordion type='single' collapsible className='space-y-4'>
{releases.map((release) => (
<AccordionItem
key={release.id}
value={release.id.toString()}
className='bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden'
>
<AccordionTrigger className='px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors'>
<div className='flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 text-left'>
<div className='flex items-center gap-2'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-white'>
{release.name || release.tag_name}
</h3>
{release.id === latestRelease?.id && (
<Badge className='bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'>
Latest
</Badge>
)}
{release.prerelease && (
<Badge
variant='outline'
className='bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300'
>
Beta
</Badge>
)}
</div>
<div className='text-sm text-gray-500 dark:text-gray-400'>
Released on {formatDate(release.published_at)}
</div>
</div>
</AccordionTrigger>
<AccordionContent className='px-6 pb-6'>
<div className='space-y-4'>
{/* Release notes */}
{(() => {
const { description, changelog } = parseReleaseNotes(
release.body || ''
)
return (
<>
{description && (
<div className='text-gray-700 dark:text-gray-300'>
{description.split('\n').map((line, i) => (
<p key={i} className='mb-2'>
{line}
</p>
))}
</div>
)}
{changelog.length > 0 && (
<div>
<h4 className='text-base font-medium mb-2 text-gray-900 dark:text-white'>
Changes:
</h4>
<ul className='space-y-1 list-disc pl-5 text-gray-600 dark:text-gray-400'>
{changelog.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
</div>
)}
</>
)
})()}
{/* Download assets */}
{release.assets.length > 0 && (
<div className='mt-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
<h4 className='text-base font-medium mb-3 text-gray-900 dark:text-white'>
Downloads:
</h4>
<div className='space-y-2'>
{release.assets.map((asset) => (
<div
key={asset.id}
className='flex flex-col sm:flex-row sm:items-center justify-between gap-2 p-3 bg-gray-50 dark:bg-gray-850 rounded-md'
>
<div className='flex items-center'>
<FileDown className='h-4 w-4 text-gray-500 dark:text-gray-400 mr-2' />
<span className='text-sm text-gray-700 dark:text-gray-300'>
{asset.name}
</span>
</div>
<div className='flex items-center gap-4'>
<span className='text-xs text-gray-500 dark:text-gray-400'>
{formatFileSize(asset.size)}
</span>
<span className='text-xs text-gray-500 dark:text-gray-400'>
<Download className='inline h-3 w-3 mr-1' />
{asset.download_count.toLocaleString()}
</span>
<Button
size='sm'
variant='outline'
className='text-xs'
asChild
>
<Link
href={asset.browser_download_url}
target='_blank'
rel='noopener noreferrer'
>
<Download className='mr-1 h-3 w-3' />
Download
</Link>
</Button>
</div>
</div>
))}
</div>
</div>
)}
<div className='flex justify-end'>
<Button
variant='ghost'
size='sm'
className='text-gray-600 dark:text-gray-400'
asChild
>
<Link
href={release.html_url}
target='_blank'
rel='noopener noreferrer'
>
<Github className='mr-2 h-4 w-4' />
View Release
</Link>
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</div>
{/* Requirements section */}
<div className='mt-16 bg-gray-50 dark:bg-gray-850 rounded-xl p-6 sm:p-8 border border-gray-200 dark:border-gray-700'>
<div className='flex items-start'>
<Info className='h-5 w-5 text-brand-600 dark:text-brand-400 mt-0.5 mr-3 flex-shrink-0' />
<div>
<h3 className='text-lg font-semibold text-gray-900 dark:text-white mb-2'>
System Requirements
</h3>
<ul className='space-y-2 text-gray-600 dark:text-gray-400'>
<li className='flex items-start'>
<Check className='h-5 w-5 text-green-500 mr-2 mt-0.5 flex-shrink-0' />
<span>Android 7.0 (Nougat) or higher</span>
</li>
<li className='flex items-start'>
<Check className='h-5 w-5 text-green-500 mr-2 mt-0.5 flex-shrink-0' />
<span>SMS capability on the Android device</span>
</li>
<li className='flex items-start'>
<Check className='h-5 w-5 text-green-500 mr-2 mt-0.5 flex-shrink-0' />
<span>Internet connection for API communication</span>
</li>
<li className='flex items-start'>
<Check className='h-5 w-5 text-green-500 mr-2 mt-0.5 flex-shrink-0' />
<span>
Battery optimization disabled for background operation
(recommended)
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,106 +1,22 @@
import { PropsWithChildren } from 'react'
import '@/styles/main.css'
import { Metadata } from 'next'
import CustomerSupport from '@/components/shared/customer-support'
import Footer from '@/components/shared/footer'
import { Toaster } from '@/components/ui/toaster'
import Analytics from '@/components/shared/analytics'
import { Session } from 'next-auth'
import { getServerSession } from 'next-auth'
import { headers } from 'next/dist/client/components/headers'
import { authOptions } from '@/lib/auth'
import prismaClient from '@/lib/prismaClient'
import { userAgent } from 'next/server'
export const metadata: Metadata = {
title: 'textbee.dev - sms gateway',
description:
'TextBee is an open-source solution that turns your Android device into a powerful SMS gateway. Send SMS effortlessly through your applications.',
authors: [
{ name: 'Israel Abebe Kokiso', url: 'https://israelabebe.com' },
{ name: 'vernu.dev', url: 'https://vernu.dev' },
],
applicationName: 'textbee.dev',
keywords: [
'textbee',
'sms gateway',
'open-source',
'android',
'sms',
'gateway',
'oss',
'free',
'opensource',
'foss',
'freeware',
'react',
'nextjs',
'tailwindcss',
'shadcn',
'typescript',
'nodejs',
'express',
'next-auth',
'vercel',
'nestjs',
],
creator: 'Israel Abebe Kokiso',
publisher: 'vernu.dev',
robots: 'index, follow',
alternates: {
canonical: 'https://textbee.dev',
},
openGraph: {
title: 'textbee.dev - sms gateway',
description:
'TextBee is an open-source solution that turns your Android device into a powerful SMS gateway. Send SMS effortlessly through your applications.',
},
icons: {
icon: '/favicon.ico',
},
title: 'textbee.dev - sms gateway - dashboard',
metadataBase: new URL('https://textbee.dev'),
}
const trackPageView = async ({
headerList,
session,
}: {
headerList: Headers
session: Session | null
}) => {
const { ua } = userAgent({
headers: headerList,
})
const url = headerList.get('x-current-url')
const ip = headerList.get('x-forwarded-for')
const referer = headerList.get('referer')
const res = await prismaClient.pageView.create({
data: {
url,
// @ts-ignore
user: session?.user?.id,
userAgent: ua,
ip,
referer,
},
})
return res
}
export default async function RootLayout({ children }: PropsWithChildren) {
const session: Session | null = await getServerSession(authOptions as any)
const headerList = headers()
trackPageView({ headerList, session })
.catch(console.error)
.then((res) => {
// console.log(res)
})
return (
<html lang='en'>
<body>
@@ -108,7 +24,6 @@ export default async function RootLayout({ children }: PropsWithChildren) {
<Analytics user={session?.user} />
<Footer />
<Toaster />
<CustomerSupport />
</body>
</html>
)

View File

@@ -1,102 +0,0 @@
import { MetadataRoute } from 'next'
import { Routes } from '@/config/routes'
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL
if (!baseUrl?.includes('textbee.dev')) {
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 1,
},
]
}
const routes = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 1,
},
{
url: `${baseUrl}#pricing`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}${Routes.useCases}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}${Routes.quickstart}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}${Routes.login}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}${Routes.register}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}${Routes.dashboard}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.9,
},
{
url: `${baseUrl}${Routes.contribute}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.7,
},
{
url: `${baseUrl}${Routes.privacyPolicy}`,
lastModified: new Date(),
changeFrequency: 'yearly' as const,
priority: 0.5,
},
{
url: `${baseUrl}${Routes.termsOfService}`,
lastModified: new Date(),
changeFrequency: 'yearly' as const,
priority: 0.5,
},
{
url: `${baseUrl}${Routes.refundPolicy}`,
lastModified: new Date(),
changeFrequency: 'yearly' as const,
priority: 0.5,
},
// {
// url: `${baseUrl}/docs`,
// lastModified: new Date(),
// changeFrequency: 'weekly' as const,
// priority: 0.9,
// },
// {
// url: `${baseUrl}/blog`,
// lastModified: new Date(),
// changeFrequency: 'weekly' as const,
// priority: 0.7,
// },
]
return routes
}

View File

@@ -2,6 +2,7 @@
import { useMemo } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
@@ -132,7 +133,7 @@ export default function AppHeader() {
<Button
asChild
color='primary'
className='bg-blue-500 hover:bg-blue-600 text-white rounded-full'
className='bg-primary hover:bg-primary/90 text-white rounded-full'
>
<Link href={Routes.register}>Get started</Link>
</Button>
@@ -151,9 +152,15 @@ export default function AppHeader() {
className='flex items-center space-x-2'
href={Routes.landingPage}
>
<MessageSquarePlus className='h-6 w-6 text-blue-500' />
<Image
src='/images/logo.png'
alt='textbee Logo'
width={24}
height={24}
className='h-6 w-6 bg-white rounded-full'
/>
<span className='font-bold'>
Text<span className='text-blue-500'>Bee</span>
text<span className='text-primary'>bee</span>
<span className='text-xs align-center text-gray-500 dark:text-gray-400'>
.dev
</span>
@@ -181,8 +188,7 @@ export default function AppHeader() {
</Button>
<Button
asChild
color='primary'
className='bg-blue-500 hover:bg-blue-600 rounded-full'
className='bg-primary hover:bg-primary/90 text-white rounded-full'
>
<Link href={Routes.register}>Get started</Link>
</Button>

View File

@@ -114,7 +114,7 @@ export default function SupportButton() {
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button
className='fixed bottom-4 right-4 shadow-lg bg-blue-500 hover:bg-blue-600 dark:text-white rounded-full'
className='fixed bottom-4 right-4 shadow-lg bg-brand-500 hover:bg-brand-600 dark:text-white rounded-full'
size='sm'
>
<MessageSquarePlus className='h-5 w-5 mr-1' />
@@ -229,8 +229,8 @@ export default function SupportButton() {
/>
{isSubmitSuccessful && (
<div className='flex items-center gap-2 text-green-500'>
<Check className='h-4 w-4' /> We received your message, we will
get back to you soon.
<Check className='h-4 w-4' /> We have received your message, we
will get back to you soon.
</div>
)}

View File

@@ -2,37 +2,45 @@ import { ExternalLinks } from '@/config/external-links'
import { Routes } from '@/config/routes'
import { MessageSquarePlus, Activity } from 'lucide-react'
import Link from 'next/link'
import Image from 'next/image'
export default function Footer() {
return (
<footer className='border-t py-6 bg-gray-50 dark:bg-muted'>
<div className='container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row'>
<div className='flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0'>
<MessageSquarePlus className='h-6 w-6 text-blue-500' />
<Image
src='/images/logo.png'
alt='textbee Logo'
width={24}
height={24}
className='h-6 w-6 bg-white rounded-full'
/>
<p className='text-center text-sm leading-loose md:text-left'>
© {new Date().getFullYear()} All rights reserved
</p>
</div>
<nav className='flex gap-4 sm:gap-6 flex-col md:flex-row items-center'>
<Link
className='text-sm font-medium hover:text-blue-500'
className='text-sm font-medium hover:text-brand-500'
href={Routes.landingPage}
>
Home
</Link>
<Link
className='text-sm font-medium hover:text-blue-500'
className='text-sm font-medium hover:text-brand-500'
href={Routes.dashboard}
>
Dashboard
</Link>
<Link
className='text-sm font-medium hover:text-blue-500'
className='text-sm font-medium hover:text-brand-500'
href={Routes.downloadAndroidApp}
>
Download App
</Link>
<Link
className='text-sm font-medium hover:text-blue-500'
className='text-sm font-medium hover:text-brand-500'
href={Routes.contribute}
target='_blank'
>
@@ -47,19 +55,19 @@ export default function Footer() {
<span className='text-green-700 dark:text-green-400'>Status</span>
</Link>
<Link
className='text-sm font-medium hover:text-blue-500'
className='text-sm font-medium hover:text-brand-500'
href={Routes.privacyPolicy}
>
Privacy Policy
</Link>
<Link
className='text-sm font-medium hover:text-blue-500'
className='text-sm font-medium hover:text-brand-500'
href={Routes.termsOfService}
>
Terms of Service
</Link>
<Link
className='text-sm font-medium hover:text-blue-500'
className='text-sm font-medium hover:text-brand-500'
href={Routes.refundPolicy}
>
Refund Policy

View File

@@ -9,7 +9,7 @@ const badgeVariants = cva(
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
"border-transparent bg-primary text-brand-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:

View File

@@ -10,7 +10,7 @@ const buttonVariants = cva(
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
"bg-primary text-brand-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:

View File

@@ -12,7 +12,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-brand-foreground",
className
)}
{...props}

View File

@@ -0,0 +1,16 @@
"use client"
import * as React from "react"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.Trigger
const CollapsibleContent = CollapsiblePrimitive.Content
export {
Collapsible,
CollapsibleTrigger,
CollapsibleContent
}

View File

@@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-brand-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}

View File

@@ -1,9 +0,0 @@
import { PrismaClient } from '@prisma/client'
const prismaClient = global.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
global.prisma = prismaClient
}
export default prismaClient

2
web/next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View File

@@ -9,6 +9,11 @@ const nextConfig = {
async redirects() {
return [
{
source: '/',
destination: '/dashboard',
permanent: true,
},
{
source: '/android',
destination: 'https://dl.textbee.dev',

View File

@@ -5,19 +5,17 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"vercel-build": "prisma generate && next build",
"prisma:generate": "prisma generate",
"prisma:studio": "prisma studio",
"vercel-build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@prisma/client": "^5.22.0",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
@@ -48,6 +46,7 @@
"react-hook-form": "^7.53.1",
"react-qr-code": "^2.0.12",
"react-syntax-highlighter": "^15.5.0",
"sharp": "^0.34.2",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3",

502
web/pnpm-lock.yaml generated
View File

@@ -11,9 +11,6 @@ importers:
'@hookform/resolvers':
specifier: ^3.9.1
version: 3.9.1(react-hook-form@7.53.1(react@18.2.0))
'@prisma/client':
specifier: ^5.22.0
version: 5.22.0(prisma@5.22.0)
'@radix-ui/react-accordion':
specifier: ^1.2.1
version: 1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -26,6 +23,9 @@ importers:
'@radix-ui/react-checkbox':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-collapsible':
specifier: ^1.1.11
version: 1.1.11(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-dialog':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -116,6 +116,9 @@ importers:
react-syntax-highlighter:
specifier: ^15.5.0
version: 15.5.0(react@18.2.0)
sharp:
specifier: ^0.34.2
version: 0.34.2
tailwind-merge:
specifier: ^2.5.4
version: 2.5.4
@@ -244,6 +247,9 @@ packages:
resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==}
engines: {node: '>=6.9.0'}
'@emnapi/runtime@1.4.3':
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
'@eslint-community/eslint-utils@4.4.0':
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -295,6 +301,122 @@ packages:
resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
deprecated: Use @eslint/object-schema instead
'@img/sharp-darwin-arm64@0.34.2':
resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.2':
resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.1.0':
resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.1.0':
resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.1.0':
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.1.0':
resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-ppc64@1.1.0':
resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
cpu: [ppc64]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.1.0':
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-x64@1.1.0':
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.34.2':
resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.34.2':
resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
'@img/sharp-linux-s390x@0.34.2':
resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-x64@0.34.2':
resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.34.2':
resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.34.2':
resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-wasm32@0.34.2':
resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.2':
resolution: {integrity: sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.2':
resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.2':
resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -617,15 +739,6 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@prisma/client@5.22.0':
resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==}
engines: {node: '>=16.13'}
peerDependencies:
prisma: '*'
peerDependenciesMeta:
prisma:
optional: true
'@prisma/debug@5.22.0':
resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==}
@@ -655,6 +768,9 @@ packages:
'@radix-ui/primitive@1.1.1':
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
'@radix-ui/react-accordion@1.2.1':
resolution: {integrity: sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ==}
peerDependencies:
@@ -746,6 +862,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-collapsible@1.1.11':
resolution: {integrity: sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.0':
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
peerDependencies:
@@ -777,6 +906,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.0':
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
peerDependencies:
@@ -795,6 +933,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.2':
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.2':
resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==}
peerDependencies:
@@ -918,6 +1065,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-label@2.1.0':
resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==}
peerDependencies:
@@ -1035,6 +1191,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.4':
resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.0.0':
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
@@ -1061,6 +1230,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.0':
resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==}
peerDependencies:
@@ -1118,6 +1300,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-switch@1.1.1':
resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==}
peerDependencies:
@@ -1188,6 +1379,24 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.2.2':
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-effect-event@0.0.2':
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.1.0':
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
peerDependencies:
@@ -1206,6 +1415,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-previous@1.1.0':
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
peerDependencies:
@@ -1834,6 +2052,13 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -1921,6 +2146,10 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
@@ -2396,6 +2625,9 @@ packages:
is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
is-async-function@2.0.0:
resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
engines: {node: '>= 0.4'}
@@ -3193,6 +3425,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@@ -3204,6 +3441,10 @@ packages:
resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==}
engines: {node: '>= 0.4'}
sharp@0.34.2:
resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -3222,6 +3463,9 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@@ -3675,6 +3919,11 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@emnapi/runtime@1.4.3':
dependencies:
tslib: 2.8.1
optional: true
'@eslint-community/eslint-utils@4.4.0(eslint@8.56.0)':
dependencies:
eslint: 8.56.0
@@ -3731,6 +3980,87 @@ snapshots:
'@humanwhocodes/object-schema@2.0.2': {}
'@img/sharp-darwin-arm64@0.34.2':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.1.0
optional: true
'@img/sharp-darwin-x64@0.34.2':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.1.0
optional: true
'@img/sharp-libvips-darwin-arm64@1.1.0':
optional: true
'@img/sharp-libvips-darwin-x64@1.1.0':
optional: true
'@img/sharp-libvips-linux-arm64@1.1.0':
optional: true
'@img/sharp-libvips-linux-arm@1.1.0':
optional: true
'@img/sharp-libvips-linux-ppc64@1.1.0':
optional: true
'@img/sharp-libvips-linux-s390x@1.1.0':
optional: true
'@img/sharp-libvips-linux-x64@1.1.0':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
optional: true
'@img/sharp-linux-arm64@0.34.2':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.1.0
optional: true
'@img/sharp-linux-arm@0.34.2':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.1.0
optional: true
'@img/sharp-linux-s390x@0.34.2':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.1.0
optional: true
'@img/sharp-linux-x64@0.34.2':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.1.0
optional: true
'@img/sharp-linuxmusl-arm64@0.34.2':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.1.0
optional: true
'@img/sharp-linuxmusl-x64@0.34.2':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
optional: true
'@img/sharp-wasm32@0.34.2':
dependencies:
'@emnapi/runtime': 1.4.3
optional: true
'@img/sharp-win32-arm64@0.34.2':
optional: true
'@img/sharp-win32-ia32@0.34.2':
optional: true
'@img/sharp-win32-x64@0.34.2':
optional: true
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -4101,10 +4431,6 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@prisma/client@5.22.0(prisma@5.22.0)':
optionalDependencies:
prisma: 5.22.0
'@prisma/debug@5.22.0': {}
'@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {}
@@ -4139,6 +4465,8 @@ snapshots:
'@radix-ui/primitive@1.1.1': {}
'@radix-ui/primitive@1.1.2': {}
'@radix-ui/react-accordion@1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -4232,6 +4560,22 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-collapsible@1.1.11(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-context': 1.1.2(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-id': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-collection@1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.48)(react@18.2.0)
@@ -4256,6 +4600,12 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-compose-refs@1.1.2(@types/react@18.2.48)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-context@1.1.0(@types/react@18.2.48)(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -4268,6 +4618,12 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-context@1.1.2(@types/react@18.2.48)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-dialog@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -4398,6 +4754,13 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-id@1.1.1(@types/react@18.2.48)(react@18.2.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-label@2.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -4531,6 +4894,16 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-presence@1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.48)(react@18.2.0)
@@ -4549,6 +4922,15 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-primitive@2.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -4626,6 +5008,13 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-slot@1.2.3(@types/react@18.2.48)(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-switch@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -4710,6 +5099,21 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.2.48)(react@18.2.0)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-use-effect-event@0.0.2(@types/react@18.2.48)(react@18.2.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.2.48)(react@18.2.0)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.48)(react@18.2.0)
@@ -4723,6 +5127,12 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.2.48)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-use-previous@1.1.0(@types/react@18.2.48)(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -5480,6 +5890,16 @@ snapshots:
color-name@1.1.4: {}
color-string@1.9.1:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@@ -5540,6 +5960,8 @@ snapshots:
dequal@2.0.3: {}
detect-libc@2.0.4: {}
detect-node-es@1.1.0: {}
didyoumean@1.2.2: {}
@@ -5681,7 +6103,7 @@ snapshots:
'@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3)
eslint: 8.56.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.56.0)
eslint-plugin-react: 7.33.2(eslint@8.56.0)
@@ -5700,12 +6122,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0):
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0):
dependencies:
debug: 4.3.4
enhanced-resolve: 5.15.0
eslint: 8.56.0
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.2
@@ -5717,14 +6139,14 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0):
eslint-module-utils@2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3)
eslint: 8.56.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0)
transitivePeerDependencies:
- supports-color
@@ -5738,7 +6160,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.56.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
hasown: 2.0.0
is-core-module: 2.13.1
is-glob: 4.0.3
@@ -6167,6 +6589,8 @@ snapshots:
get-intrinsic: 1.3.0
is-typed-array: 1.1.12
is-arrayish@0.3.2: {}
is-async-function@2.0.0:
dependencies:
has-tostringtag: 1.0.2
@@ -6924,6 +7348,8 @@ snapshots:
dependencies:
lru-cache: 6.0.0
semver@7.7.2: {}
serialize-javascript@6.0.2:
dependencies:
randombytes: 2.1.0
@@ -6942,6 +7368,34 @@ snapshots:
functions-have-names: 1.2.3
has-property-descriptors: 1.0.1
sharp@0.34.2:
dependencies:
color: 4.2.3
detect-libc: 2.0.4
semver: 7.7.2
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.2
'@img/sharp-darwin-x64': 0.34.2
'@img/sharp-libvips-darwin-arm64': 1.1.0
'@img/sharp-libvips-darwin-x64': 1.1.0
'@img/sharp-libvips-linux-arm': 1.1.0
'@img/sharp-libvips-linux-arm64': 1.1.0
'@img/sharp-libvips-linux-ppc64': 1.1.0
'@img/sharp-libvips-linux-s390x': 1.1.0
'@img/sharp-libvips-linux-x64': 1.1.0
'@img/sharp-libvips-linuxmusl-arm64': 1.1.0
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
'@img/sharp-linux-arm': 0.34.2
'@img/sharp-linux-arm64': 0.34.2
'@img/sharp-linux-s390x': 0.34.2
'@img/sharp-linux-x64': 0.34.2
'@img/sharp-linuxmusl-arm64': 0.34.2
'@img/sharp-linuxmusl-x64': 0.34.2
'@img/sharp-wasm32': 0.34.2
'@img/sharp-win32-arm64': 0.34.2
'@img/sharp-win32-ia32': 0.34.2
'@img/sharp-win32-x64': 0.34.2
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -6958,6 +7412,10 @@ snapshots:
signal-exit@4.1.0: {}
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
slash@3.0.0: {}
source-map-js@1.2.1: {}

View File

@@ -1,34 +0,0 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model supportMessage {
id String @id @default(auto()) @map("_id") @db.ObjectId
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user String? @db.ObjectId
name String?
email String?
phone String?
category String?
message String?
ip String?
userAgent String?
type String @default("RECEIVED")
}
model pageView {
id String @id @default(auto()) @map("_id") @db.ObjectId
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user String? @db.ObjectId
ip String?
userAgent String?
referer String?
url String?
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

BIN
web/public/images/logo.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
web/public/oldfavicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -9,8 +9,8 @@
--card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--primary: 220.9 39.3% 11%;
--primary-foreground: 210 20% 98%;
--primary: 38 92% 44%;
--brand-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--muted: 220 14.3% 95.9%;
@@ -36,8 +36,8 @@
--card-foreground: 0 0% 98%;
--popover: 0 0% 15%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 15%;
--primary: 38 92% 44%;
--brand-foreground: 0 0% 15%;
--secondary: 0 0% 25%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 25%;

View File

@@ -1,86 +1,99 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
darkMode: ['class'],
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
// Or if using `src` directory:
"./src/**/*.{js,ts,jsx,tsx,mdx}",
'./src/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--brand-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
1: 'hsl(var(--chart-1))',
2: 'hsl(var(--chart-2))',
3: 'hsl(var(--chart-3))',
4: 'hsl(var(--chart-4))',
5: 'hsl(var(--chart-5))',
},
brand: {
50: '#FFFBEB',
100: '#FEF3C7',
200: '#FDE68A',
300: '#FCD34D',
400: '#FBBF24',
500: '#D97706',
600: '#B45309',
700: '#92400E',
800: '#78350F',
900: '#451A03',
950: '#2C0D02',
},
},
keyframes: {
'accordion-down': {
from: {
height: '0',
},
to: {
height: 'var(--radix-accordion-content-height)',
},
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)',
},
to: {
height: '0',
},
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [require("tailwindcss-animate")],
}
plugins: [require('tailwindcss-animate')],
}