mirror of
https://github.com/vernu/textbee.git
synced 2026-02-20 07:34:00 -05:00
Merge pull request #81 from vernu/web-dashboard-ui
UI/UX redesign of web dashboard
This commit is contained in:
1
.github/workflows/build-and-test.yaml
vendored
1
.github/workflows/build-and-test.yaml
vendored
@@ -62,7 +62,6 @@ jobs:
|
||||
run: |
|
||||
cd web
|
||||
pnpm install
|
||||
pnpm run prisma:generate
|
||||
pnpm run build
|
||||
|
||||
build-and-test-android:
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
155
web/app/(app)/dashboard/(components)/change-password-form.tsx
Normal file
155
web/app/(app)/dashboard/(components)/change-password-form.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
160
web/app/(app)/dashboard/(components)/danger-zone-form.tsx
Normal file
160
web/app/(app)/dashboard/(components)/danger-zone-form.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
192
web/app/(app)/dashboard/(components)/edit-profile-form.tsx
Normal file
192
web/app/(app)/dashboard/(components)/edit-profile-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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'>
|
||||
|
||||
266
web/app/(app)/dashboard/(components)/subscription-info.tsx
Normal file
266
web/app/(app)/dashboard/(components)/subscription-info.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
209
web/app/(app)/dashboard/(components)/support-form.tsx
Normal file
209
web/app/(app)/dashboard/(components)/support-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
web/app/(app)/dashboard/account/change-password/page.tsx
Normal file
29
web/app/(app)/dashboard/account/change-password/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
34
web/app/(app)/dashboard/account/delete-account/page.tsx
Normal file
34
web/app/(app)/dashboard/account/delete-account/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
web/app/(app)/dashboard/account/edit-profile/page.tsx
Normal file
29
web/app/(app)/dashboard/account/edit-profile/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
web/app/(app)/dashboard/account/get-support/page.tsx
Normal file
29
web/app/(app)/dashboard/account/get-support/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
95
web/app/(app)/dashboard/account/page.tsx
Normal file
95
web/app/(app)/dashboard/account/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
20
web/app/(app)/dashboard/community/page.tsx
Normal file
20
web/app/(app)/dashboard/community/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
420
web/app/(app)/dashboard/messaging/(components)/api-guide.tsx
Normal file
420
web/app/(app)/dashboard/messaging/(components)/api-guide.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
31
web/app/(app)/dashboard/messaging/page.tsx
Normal file
31
web/app/(app)/dashboard/messaging/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
(“Platform”). 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 (“Terms”) govern your access to and use of our services, including our website, mobile applications, APIs, and other software (“Services”). 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 “Software”), 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 “AS IS”, 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 “as is”, 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>
|
||||
)
|
||||
}
|
||||
@@ -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
536
web/app/download/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
|
||||
16
web/components/ui/collapsible.tsx
Normal file
16
web/components/ui/collapsible.tsx
Normal 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
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
2
web/next-env.d.ts
vendored
@@ -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.
|
||||
|
||||
@@ -9,6 +9,11 @@ const nextConfig = {
|
||||
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/dashboard',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/android',
|
||||
destination: 'https://dl.textbee.dev',
|
||||
|
||||
@@ -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
502
web/pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
web/public/images/app-screenshot.png
Normal file
BIN
web/public/images/app-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 320 KiB |
BIN
web/public/images/app-screenshot2.png
Normal file
BIN
web/public/images/app-screenshot2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
BIN
web/public/images/logo.png
Normal file
BIN
web/public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
BIN
web/public/oldfavicon.ico
Normal file
BIN
web/public/oldfavicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -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%;
|
||||
|
||||
@@ -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')],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user