From db3ac97af89f137a72f9c7a9bd3b002c03cd5aa0 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 4 Jun 2025 20:37:23 +0300 Subject: [PATCH 1/2] ui(web): rebuild web dashboard ui/ux --- .../(app)/(auth)/(components)/login-form.tsx | 8 +- .../request-password-reset-form.tsx | 2 +- .../(components)/reset-password-form.tsx | 2 +- web/app/(app)/(auth)/login/page.tsx | 4 +- web/app/(app)/(auth)/register/page.tsx | 2 +- web/app/(app)/(auth)/verify-email/page.tsx | 6 +- web/app/(app)/checkout/[planName]/page.tsx | 43 +- .../(components)/account-settings.tsx | 104 +-- .../(app)/dashboard/(components)/api-keys.tsx | 2 - .../dashboard/(components)/bulk-sms-send.tsx | 2 +- .../(components)/change-password-form.tsx | 155 +++++ .../(components)/community-links.tsx | 10 +- .../(components)/danger-zone-form.tsx | 160 +++++ .../dashboard/(components)/device-list.tsx | 2 - .../(components)/edit-profile-form.tsx | 192 ++++++ .../dashboard/(components)/get-started.tsx | 60 +- .../dashboard/(components)/main-dashboard.tsx | 2 +- .../(components)/message-history.tsx | 189 +++++- .../dashboard/(components)/messaging.tsx | 4 +- .../(app)/dashboard/(components)/overview.tsx | 21 +- .../(app)/dashboard/(components)/send-sms.tsx | 2 +- .../(components)/subscription-info.tsx | 266 ++++++++ .../dashboard/(components)/support-form.tsx | 209 ++++++ .../account/change-password/page.tsx | 29 + .../dashboard/account/delete-account/page.tsx | 34 + .../dashboard/account/edit-profile/page.tsx | 29 + .../dashboard/account/get-support/page.tsx | 29 + web/app/(app)/dashboard/account/page.tsx | 95 +++ web/app/(app)/dashboard/community/page.tsx | 20 + web/app/(app)/dashboard/layout.tsx | 158 ++++- .../messaging/(components)/api-guide.tsx | 420 ++++++++++++ web/app/(app)/dashboard/messaging/page.tsx | 31 + web/app/(app)/dashboard/page.tsx | 43 +- .../(components)/code-snippet-section.tsx | 100 --- .../(components)/customization-section.tsx | 49 -- .../(components)/download-app-section.tsx | 36 - .../(components)/features-section.tsx | 48 -- .../(components)/hero-section.tsx | 75 --- .../(components)/how-it-works-section.tsx | 57 -- .../(components)/landing-page-header.tsx | 91 --- .../(components)/pricing-section.tsx | 195 ------ .../(components)/support-project-section.tsx | 116 ---- web/app/(landing-page)/layout.tsx | 11 - web/app/(landing-page)/page.tsx | 24 - .../(landing-page)/privacy-policy/page.tsx | 80 --- web/app/(landing-page)/quickstart/page.tsx | 581 ---------------- web/app/(landing-page)/refund-policy/page.tsx | 161 ----- .../(landing-page)/terms-of-service/page.tsx | 139 ---- web/app/(landing-page)/use-cases/page.tsx | 630 ------------------ web/app/download/page.tsx | 536 +++++++++++++++ web/app/layout.tsx | 89 +-- web/app/sitemap.ts | 102 --- web/components/shared/app-header.tsx | 16 +- web/components/shared/customer-support.tsx | 6 +- web/components/shared/footer.tsx | 24 +- web/components/ui/badge.tsx | 2 +- web/components/ui/button.tsx | 2 +- web/components/ui/checkbox.tsx | 2 +- web/components/ui/collapsible.tsx | 16 + web/components/ui/tooltip.tsx | 2 +- web/lib/prismaClient.ts | 9 - web/next-env.d.ts | 2 +- web/next.config.js | 5 + web/package.json | 7 +- web/pnpm-lock.yaml | 502 +++++++++++++- web/prisma/schema.prisma | 34 - web/public/favicon.ico | Bin 15406 -> 4286 bytes web/public/images/app-screenshot.png | Bin 0 -> 327654 bytes web/public/images/app-screenshot2.png | Bin 0 -> 303595 bytes web/public/images/logo.png | Bin 0 -> 118363 bytes web/public/oldfavicon.ico | Bin 0 -> 15406 bytes web/styles/main.css | 8 +- web/tailwind.config.js | 173 ++--- 73 files changed, 3369 insertions(+), 2896 deletions(-) create mode 100644 web/app/(app)/dashboard/(components)/change-password-form.tsx create mode 100644 web/app/(app)/dashboard/(components)/danger-zone-form.tsx create mode 100644 web/app/(app)/dashboard/(components)/edit-profile-form.tsx create mode 100644 web/app/(app)/dashboard/(components)/subscription-info.tsx create mode 100644 web/app/(app)/dashboard/(components)/support-form.tsx create mode 100644 web/app/(app)/dashboard/account/change-password/page.tsx create mode 100644 web/app/(app)/dashboard/account/delete-account/page.tsx create mode 100644 web/app/(app)/dashboard/account/edit-profile/page.tsx create mode 100644 web/app/(app)/dashboard/account/get-support/page.tsx create mode 100644 web/app/(app)/dashboard/account/page.tsx create mode 100644 web/app/(app)/dashboard/community/page.tsx create mode 100644 web/app/(app)/dashboard/messaging/(components)/api-guide.tsx create mode 100644 web/app/(app)/dashboard/messaging/page.tsx delete mode 100644 web/app/(landing-page)/(components)/code-snippet-section.tsx delete mode 100644 web/app/(landing-page)/(components)/customization-section.tsx delete mode 100644 web/app/(landing-page)/(components)/download-app-section.tsx delete mode 100644 web/app/(landing-page)/(components)/features-section.tsx delete mode 100644 web/app/(landing-page)/(components)/hero-section.tsx delete mode 100644 web/app/(landing-page)/(components)/how-it-works-section.tsx delete mode 100644 web/app/(landing-page)/(components)/landing-page-header.tsx delete mode 100644 web/app/(landing-page)/(components)/pricing-section.tsx delete mode 100644 web/app/(landing-page)/(components)/support-project-section.tsx delete mode 100644 web/app/(landing-page)/layout.tsx delete mode 100644 web/app/(landing-page)/page.tsx delete mode 100644 web/app/(landing-page)/privacy-policy/page.tsx delete mode 100644 web/app/(landing-page)/quickstart/page.tsx delete mode 100644 web/app/(landing-page)/refund-policy/page.tsx delete mode 100644 web/app/(landing-page)/terms-of-service/page.tsx delete mode 100644 web/app/(landing-page)/use-cases/page.tsx create mode 100644 web/app/download/page.tsx delete mode 100644 web/app/sitemap.ts create mode 100644 web/components/ui/collapsible.tsx delete mode 100644 web/lib/prismaClient.ts delete mode 100644 web/prisma/schema.prisma create mode 100644 web/public/images/app-screenshot.png create mode 100644 web/public/images/app-screenshot2.png create mode 100644 web/public/images/logo.png create mode 100644 web/public/oldfavicon.ico diff --git a/web/app/(app)/(auth)/(components)/login-form.tsx b/web/app/(app)/(auth)/(components)/login-form.tsx index 3bbd8b2..141e10a 100644 --- a/web/app/(app)/(auth)/(components)/login-form.tsx +++ b/web/app/(app)/(auth)/(components)/login-form.tsx @@ -88,10 +88,10 @@ export default function LoginForm() { Password - diff --git a/web/app/(app)/(auth)/(components)/request-password-reset-form.tsx b/web/app/(app)/(auth)/(components)/request-password-reset-form.tsx index 2bdcf42..7df481d 100644 --- a/web/app/(app)/(auth)/(components)/request-password-reset-form.tsx +++ b/web/app/(app)/(auth)/(components)/request-password-reset-form.tsx @@ -123,7 +123,7 @@ export default function RequestPasswordResetForm() { Back to login diff --git a/web/app/(app)/(auth)/(components)/reset-password-form.tsx b/web/app/(app)/(auth)/(components)/reset-password-form.tsx index 80b0a53..54ef108 100644 --- a/web/app/(app)/(auth)/(components)/reset-password-form.tsx +++ b/web/app/(app)/(auth)/(components)/reset-password-form.tsx @@ -188,7 +188,7 @@ export default function ResetPasswordForm({ Back to login diff --git a/web/app/(app)/(auth)/login/page.tsx b/web/app/(app)/(auth)/login/page.tsx index 7716a33..8daa396 100644 --- a/web/app/(app)/(auth)/login/page.tsx +++ b/web/app/(app)/(auth)/login/page.tsx @@ -50,7 +50,7 @@ export default function LoginPage() { Forgot your password? @@ -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 diff --git a/web/app/(app)/(auth)/register/page.tsx b/web/app/(app)/(auth)/register/page.tsx index 66850b7..a63cbaa 100644 --- a/web/app/(app)/(auth)/register/page.tsx +++ b/web/app/(app)/(auth)/register/page.tsx @@ -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 diff --git a/web/app/(app)/(auth)/verify-email/page.tsx b/web/app/(app)/(auth)/verify-email/page.tsx index 813a7fe..002b849 100644 --- a/web/app/(app)/(auth)/verify-email/page.tsx +++ b/web/app/(app)/(auth)/verify-email/page.tsx @@ -40,8 +40,8 @@ const SuccessAlert = ({ title, message }: { title: string; message: string }) => ) const InfoAlert = ({ title, message }: { title: string; message: string }) => ( - - + + {title} {message} @@ -54,7 +54,7 @@ const LoadingSpinner = () => ( ) const DashboardButton = () => ( - @@ -61,7 +64,7 @@ export default function CheckoutPage({ params }) { return (
- +

Hang Tight!

We're processing your order. This won't take long! diff --git a/web/app/(app)/dashboard/(components)/account-settings.tsx b/web/app/(app)/dashboard/(components)/account-settings.tsx index 6836698..bad9404 100644 --- a/web/app/(app)/dashboard/(components)/account-settings.tsx +++ b/web/app/(app)/dashboard/(components)/account-settings.tsx @@ -218,7 +218,7 @@ export default function AccountSettings() { if (isLoadingSubscription) return ( -

+
) @@ -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 (
@@ -259,40 +262,54 @@ export default function AccountSettings() { Current subscription

{currentSubscription?.amount > 0 && ( - - {formatPrice(currentSubscription?.amount, currentSubscription?.currency)} + + {formatPrice( + currentSubscription?.amount, + currentSubscription?.currency + )} {currentSubscription?.recurringInterval && ( - / {getBillingInterval(currentSubscription?.recurringInterval)} + + /{' '} + {getBillingInterval( + currentSubscription?.recurringInterval + )} + )} )}
-
- + - + - {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'} @@ -301,7 +318,7 @@ export default function AccountSettings() {
- +

Start Date @@ -321,7 +338,7 @@ export default function AccountSettings() {

- +

Next Payment @@ -376,7 +393,8 @@ export default function AccountSettings() {

{currentSubscription?.plan?.monthlyLimit === -1 ? 'Unlimited' - : currentSubscription?.plan?.monthlyLimit?.toLocaleString() || '0'} + : currentSubscription?.plan?.monthlyLimit?.toLocaleString() || + '0'} {currentSubscription?.plan?.monthlyLimit === -1 && ( @@ -420,10 +438,11 @@ export default function AccountSettings() {

- {(!currentSubscription?.plan?.name || currentSubscription?.plan?.name?.toLowerCase() === 'free') ? ( + {!currentSubscription?.plan?.name || + currentSubscription?.plan?.name?.toLowerCase() === 'free' ? ( Upgrade to Pro → @@ -442,13 +461,13 @@ export default function AccountSettings() { if (isLoadingUser) return ( -
+
) return ( -
+
@@ -599,7 +618,7 @@ export default function AccountSettings() {
- + - {(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'}

)} diff --git a/web/app/(app)/dashboard/(components)/api-keys.tsx b/web/app/(app)/dashboard/(components)/api-keys.tsx index c46ffc4..3351426 100644 --- a/web/app/(app)/dashboard/(components)/api-keys.tsx +++ b/web/app/(app)/dashboard/(components)/api-keys.tsx @@ -129,7 +129,6 @@ export default function ApiKeys() { API Keys -
{isPending && ( <> @@ -255,7 +254,6 @@ export default function ApiKeys() { ))}
-
{/* Revoke Dialog */} diff --git a/web/app/(app)/dashboard/(components)/bulk-sms-send.tsx b/web/app/(app)/dashboard/(components)/bulk-sms-send.tsx index b5a8851..f6312b9 100644 --- a/web/app/(app)/dashboard/(components)/bulk-sms-send.tsx +++ b/web/app/(app)/dashboard/(components)/bulk-sms-send.tsx @@ -143,7 +143,7 @@ export default function BulkSMSSend() { const isStep3Disabled = isStep2Disabled || !selectedColumn || !messageTemplate return ( -
+
Send Bulk SMS diff --git a/web/app/(app)/dashboard/(components)/change-password-form.tsx b/web/app/(app)/dashboard/(components)/change-password-form.tsx new file mode 100644 index 0000000..599f821 --- /dev/null +++ b/web/app/(app)/dashboard/(components)/change-password-form.tsx @@ -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 + +export default function ChangePasswordForm() { + const { toast } = useToast() + + const changePasswordForm = useForm({ + 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 ( + <> +

+ If you signed in with Google, you can reset your password{' '} + + here + + . +

+ +
changePassword(data))} + className='space-y-4' + > +
+ + + {changePasswordForm.formState.errors.oldPassword && ( +

+ {changePasswordForm.formState.errors.oldPassword.message} +

+ )} +
+ +
+ + + {changePasswordForm.formState.errors.newPassword && ( +

+ {changePasswordForm.formState.errors.newPassword.message} +

+ )} +
+ +
+ + + {changePasswordForm.formState.errors.confirmPassword && ( +

+ {changePasswordForm.formState.errors.confirmPassword.message} +

+ )} +
+ + {changePasswordForm.formState.errors.root?.serverError && ( +

+ {changePasswordForm.formState.errors.root.serverError.message} +

+ )} + + {isChangePasswordSuccess && ( +

+ Password changed successfully! +

+ )} + + +
+ + ) +} \ No newline at end of file diff --git a/web/app/(app)/dashboard/(components)/community-links.tsx b/web/app/(app)/dashboard/(components)/community-links.tsx index 6b32b22..506f3b8 100644 --- a/web/app/(app)/dashboard/(components)/community-links.tsx +++ b/web/app/(app)/dashboard/(components)/community-links.tsx @@ -6,8 +6,8 @@ import { ExternalLinks } from '@/config/external-links' export default function CommunityLinks() { return ( -
- +
+ {/* One-time Donation @@ -22,9 +22,9 @@ export default function CommunityLinks() { - + */} - + {/* Support on Patreon @@ -39,7 +39,7 @@ export default function CommunityLinks() { - + */} diff --git a/web/app/(app)/dashboard/(components)/danger-zone-form.tsx b/web/app/(app)/dashboard/(components)/danger-zone-form.tsx new file mode 100644 index 0000000..3e30b00 --- /dev/null +++ b/web/app/(app)/dashboard/(components)/danger-zone-form.tsx @@ -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 ( + <> +

+ 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. +

+ + + + + + + + Delete Account + + +

+ Are you sure you want to delete your account? This action: +

+
    +
  • Cannot be undone
  • +
  • Will permanently delete all your data
  • +
  • Will cancel all active subscriptions
  • +
  • Will remove access to all services
  • +
+ + +