From 4ce4dfe9b8529442fbff25f8e7c0a78592a6e2a4 Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Tue, 16 Sep 2025 21:57:55 -0700
Subject: [PATCH 01/18] fix: biome check
---
.../components/settings/model-config-manager.tsx | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx
index a7f5d2dec..bc59532d1 100644
--- a/surfsense_web/components/settings/model-config-manager.tsx
+++ b/surfsense_web/components/settings/model-config-manager.tsx
@@ -112,12 +112,12 @@ const LLM_PROVIDERS = [
example: "meta/llama-2-70b-chat",
description: "Run models via API",
},
- {
- value: "OPENROUTER",
- label: "OpenRouter",
- example: "anthropic/claude-opus-4.1, openai/gpt-5",
- description: "API gateway and LLM marketplace that provides unified access ",
- },
+ {
+ value: "OPENROUTER",
+ label: "OpenRouter",
+ example: "anthropic/claude-opus-4.1, openai/gpt-5",
+ description: "API gateway and LLM marketplace that provides unified access ",
+ },
{
value: "CUSTOM",
label: "Custom Provider",
From 2fd77b74c9906f0904f0481d7011ba98cf577742 Mon Sep 17 00:00:00 2001
From: samkul-swe
Date: Thu, 18 Sep 2025 14:52:46 -0700
Subject: [PATCH 02/18] fix for 322 - shows relevant messages and toasts
---
surfsense_web/app/login/LocalLoginForm.tsx | 145 +++++++++++++--
surfsense_web/app/login/page.tsx | 131 ++++++++++++-
surfsense_web/app/register/page.tsx | 161 ++++++++++++++--
surfsense_web/lib/auth-errors.ts | 207 +++++++++++++++++++++
4 files changed, 603 insertions(+), 41 deletions(-)
create mode 100644 surfsense_web/lib/auth-errors.ts
diff --git a/surfsense_web/app/login/LocalLoginForm.tsx b/surfsense_web/app/login/LocalLoginForm.tsx
index 7cc50b28a..2a4d9239a 100644
--- a/surfsense_web/app/login/LocalLoginForm.tsx
+++ b/surfsense_web/app/login/LocalLoginForm.tsx
@@ -2,11 +2,15 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { toast } from "sonner";
+import { getAuthErrorDetails, shouldRetry, isNetworkError } from "@/lib/auth-errors";
export function LocalLoginForm() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
- const [error, setError] = useState("");
+ const [error, setError] = useState(null);
+ const [errorTitle, setErrorTitle] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [authType, setAuthType] = useState(null);
const router = useRouter();
@@ -19,7 +23,11 @@ export function LocalLoginForm() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
- setError("");
+ setError(null); // Clear any previous errors
+ setErrorTitle(null);
+
+ // Show loading toast
+ const loadingToast = toast.loading("Signing you in...");
try {
// Create form data for the API request
@@ -42,13 +50,54 @@ export function LocalLoginForm() {
const data = await response.json();
if (!response.ok) {
- throw new Error(data.detail || "Failed to login");
+ throw new Error(data.detail || `HTTP ${response.status}`);
}
- router.push(`/auth/callback?token=${data.access_token}`);
+ // Success toast
+ toast.success("Login successful!", {
+ id: loadingToast,
+ description: "Redirecting to dashboard...",
+ duration: 2000,
+ });
+
+ // Small delay to show success message
+ setTimeout(() => {
+ router.push(`/auth/callback?token=${data.access_token}`);
+ }, 500);
+
} catch (err) {
- const errorMessage = err instanceof Error ? err.message : "An error occurred during login";
- setError(errorMessage);
+ // Use auth-errors utility to get proper error details
+ let errorCode = "UNKNOWN_ERROR";
+
+ if (err instanceof Error) {
+ errorCode = err.message;
+ } else if (isNetworkError(err)) {
+ errorCode = "NETWORK_ERROR";
+ }
+
+ // Get detailed error information from auth-errors utility
+ const errorDetails = getAuthErrorDetails(errorCode);
+
+ // Set persistent error display
+ setErrorTitle(errorDetails.title);
+ setError(errorDetails.description);
+
+ // Show error toast with conditional retry action
+ const toastOptions: any = {
+ id: loadingToast,
+ description: errorDetails.description,
+ duration: 6000,
+ };
+
+ // Add retry action if the error is retryable
+ if (shouldRetry(errorCode)) {
+ toastOptions.action = {
+ label: "Retry",
+ onClick: () => handleSubmit(e),
+ };
+ }
+
+ toast.error(errorDetails.title, toastOptions);
} finally {
setIsLoading(false);
}
@@ -57,11 +106,69 @@ export function LocalLoginForm() {
return (
diff --git a/surfsense_web/app/login/page.tsx b/surfsense_web/app/login/page.tsx
index 1957c22bf..209dc745a 100644
--- a/surfsense_web/app/login/page.tsx
+++ b/surfsense_web/app/login/page.tsx
@@ -3,6 +3,9 @@
import { Loader2 } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { toast } from "sonner";
+import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors";
import { Logo } from "@/components/Logo";
import { AmbientBackground } from "./AmbientBackground";
import { GoogleLoginButton } from "./GoogleLoginButton";
@@ -10,14 +13,70 @@ import { LocalLoginForm } from "./LocalLoginForm";
function LoginContent() {
const [authType, setAuthType] = useState(null);
- const [registrationSuccess, setRegistrationSuccess] = useState(false);
const [isLoading, setIsLoading] = useState(true);
+ const [urlError, setUrlError] = useState<{ title: string; message: string } | null>(null);
const searchParams = useSearchParams();
useEffect(() => {
- // Check if the user was redirected from registration
- if (searchParams.get("registered") === "true") {
- setRegistrationSuccess(true);
+ // Check for various URL parameters that might indicate success or error states
+ const registered = searchParams.get("registered");
+ const error = searchParams.get("error");
+ const message = searchParams.get("message");
+ const logout = searchParams.get("logout");
+
+ // Show registration success message
+ if (registered === "true") {
+ toast.success("Registration successful!", {
+ description: "You can now sign in with your credentials",
+ duration: 5000,
+ });
+ }
+
+ // Show logout confirmation
+ if (logout === "true") {
+ toast.success("Logged out successfully", {
+ description: "You have been securely logged out",
+ duration: 3000,
+ });
+ }
+
+ // Show error messages from OAuth or other flows using auth-errors utility
+ if (error) {
+ // Use the auth-errors utility to get proper error details
+ const errorDetails = getAuthErrorDetails(error);
+
+ // If we have a custom message from URL params, use it as description
+ const errorDescription = message ? decodeURIComponent(message) : errorDetails.description;
+
+ // Set persistent error display
+ setUrlError({
+ title: errorDetails.title,
+ message: errorDescription
+ });
+
+ // Show toast with conditional retry action
+ const toastOptions: any = {
+ description: errorDescription,
+ duration: 6000,
+ };
+
+ // Add retry action if the error is retryable
+ if (shouldRetry(error)) {
+ toastOptions.action = {
+ label: "Retry",
+ onClick: () => window.location.reload(),
+ };
+ }
+
+ toast.error(errorDetails.title, toastOptions);
+ }
+
+ // Show general messages
+ if (message && !error && !registered && !logout) {
+ toast.info("Notice", {
+ description: decodeURIComponent(message),
+ duration: 4000,
+ });
}
// Get the auth type from environment variables
@@ -54,11 +113,65 @@ function LoginContent() {
Sign In
- {registrationSuccess && (
-
- Registration successful! You can now sign in with your credentials.
-
- )}
+ {/* URL Error Display */}
+
+ {urlError && (
+
+
+
+
+
{urlError.title}
+
+ {urlError.message}
+
+
+
+
+
+ )}
+
diff --git a/surfsense_web/app/register/page.tsx b/surfsense_web/app/register/page.tsx
index 1a89dbb4e..28559c991 100644
--- a/surfsense_web/app/register/page.tsx
+++ b/surfsense_web/app/register/page.tsx
@@ -3,6 +3,9 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { toast } from "sonner";
+import { getAuthErrorDetails, shouldRetry, isNetworkError } from "@/lib/auth-errors";
import { Logo } from "@/components/Logo";
import { AmbientBackground } from "../login/AmbientBackground";
@@ -10,7 +13,8 @@ export default function RegisterPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
- const [error, setError] = useState("");
+ const [error, setError] = useState(null);
+ const [errorTitle, setErrorTitle] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
@@ -28,11 +32,20 @@ export default function RegisterPage() {
// Form validation
if (password !== confirmPassword) {
setError("Passwords do not match");
+ setErrorTitle("Password Mismatch");
+ toast.error("Password Mismatch", {
+ description: "The passwords you entered do not match",
+ duration: 4000,
+ });
return;
}
setIsLoading(true);
- setError("");
+ setError(null); // Clear any previous errors
+ setErrorTitle(null);
+
+ // Show loading toast
+ const loadingToast = toast.loading("Creating your account...");
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/register`, {
@@ -52,15 +65,54 @@ export default function RegisterPage() {
const data = await response.json();
if (!response.ok) {
- throw new Error(data.detail || "Registration failed");
+ throw new Error(data.detail || `HTTP ${response.status}`);
}
- // Redirect to login page after successful registration
- router.push("/login?registered=true");
- } catch (err: unknown) {
- const errorMessage =
- err instanceof Error ? err.message : "An error occurred during registration";
- setError(errorMessage);
+ // Success toast
+ toast.success("Account created successfully!", {
+ id: loadingToast,
+ description: "Redirecting to login page...",
+ duration: 2000,
+ });
+
+ // Small delay to show success message
+ setTimeout(() => {
+ router.push("/login?registered=true");
+ }, 500);
+
+ } catch (err) {
+ // Use auth-errors utility to get proper error details
+ let errorCode = "UNKNOWN_ERROR";
+
+ if (err instanceof Error) {
+ errorCode = err.message;
+ } else if (isNetworkError(err)) {
+ errorCode = "NETWORK_ERROR";
+ }
+
+ // Get detailed error information from auth-errors utility
+ const errorDetails = getAuthErrorDetails(errorCode);
+
+ // Set persistent error display
+ setErrorTitle(errorDetails.title);
+ setError(errorDetails.description);
+
+ // Show error toast with conditional retry action
+ const toastOptions: any = {
+ id: loadingToast,
+ description: errorDetails.description,
+ duration: 6000,
+ };
+
+ // Add retry action if the error is retryable
+ if (shouldRetry(errorCode)) {
+ toastOptions.action = {
+ label: "Retry",
+ onClick: () => handleSubmit(e),
+ };
+ }
+
+ toast.error(errorDetails.title, toastOptions);
} finally {
setIsLoading(false);
}
@@ -77,11 +129,69 @@ export default function RegisterPage() {
@@ -130,14 +250,19 @@ export default function RegisterPage() {
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
- className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
+ className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
+ error
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
+ : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
+ }`}
+ disabled={isLoading}
/>
diff --git a/surfsense_web/lib/auth-errors.ts b/surfsense_web/lib/auth-errors.ts
new file mode 100644
index 000000000..1858bd45c
--- /dev/null
+++ b/surfsense_web/lib/auth-errors.ts
@@ -0,0 +1,207 @@
+/**
+ * Authentication error messages and handling utilities
+ */
+
+interface AuthErrorMapping {
+ [key: string]: {
+ title: string;
+ description?: string;
+ };
+}
+
+const AUTH_ERROR_MESSAGES: AuthErrorMapping = {
+ // Common HTTP errors
+ "401": {
+ title: "Invalid credentials",
+ description: "Please check your email and password"
+ },
+ "403": {
+ title: "Access denied",
+ description: "Your account may be suspended or restricted"
+ },
+ "404": {
+ title: "Account not found",
+ description: "No account exists with this email address"
+ },
+ "409": {
+ title: "Account conflict",
+ description: "An account with this email already exists"
+ },
+ "429": {
+ title: "Too many attempts",
+ description: "Please wait before trying again"
+ },
+ "500": {
+ title: "Server error",
+ description: "Something went wrong on our end. Please try again"
+ },
+ "503": {
+ title: "Service unavailable",
+ description: "Login service is temporarily down"
+ },
+
+ // FastAPI specific errors
+ "LOGIN_BAD_CREDENTIALS": {
+ title: "Invalid credentials",
+ description: "The email or password you entered is incorrect"
+ },
+ "LOGIN_USER_NOT_VERIFIED": {
+ title: "Account not verified",
+ description: "Please verify your email address before signing in"
+ },
+ "USER_INACTIVE": {
+ title: "Account inactive",
+ description: "Your account has been deactivated. Contact support for assistance"
+ },
+ "REGISTER_USER_ALREADY_EXISTS": {
+ title: "Account already exists",
+ description: "An account with this email address already exists"
+ },
+ "REGISTER_INVALID_PASSWORD": {
+ title: "Invalid password",
+ description: "Password must meet security requirements"
+ },
+
+ // OAuth errors
+ "access_denied": {
+ title: "Access denied",
+ description: "You denied access or cancelled the login process"
+ },
+ "invalid_request": {
+ title: "Invalid request",
+ description: "The login request was malformed"
+ },
+ "unauthorized_client": {
+ title: "Authentication failed",
+ description: "The application is not authorized to perform this action"
+ },
+ "unsupported_response_type": {
+ title: "Login method not supported",
+ description: "This login method is not currently available"
+ },
+ "invalid_scope": {
+ title: "Invalid permissions",
+ description: "The requested permissions are not valid"
+ },
+ "server_error": {
+ title: "Server error",
+ description: "An error occurred on the authentication server"
+ },
+ "temporarily_unavailable": {
+ title: "Service unavailable",
+ description: "Login is temporarily unavailable. Please try again later"
+ },
+
+ // Network errors
+ "NETWORK_ERROR": {
+ title: "Connection failed",
+ description: "Please check your internet connection and try again"
+ },
+ "TIMEOUT": {
+ title: "Request timeout",
+ description: "The login request took too long. Please try again"
+ },
+
+ // Generic fallbacks
+ "UNKNOWN_ERROR": {
+ title: "Login failed",
+ description: "An unexpected error occurred. Please try again"
+ }
+};
+
+/**
+ * Get a user-friendly error message for authentication errors
+ * @param errorCode - The error code or message from the API
+ * @param returnTitle - Whether to return just the title or full description
+ * @returns Formatted error message
+ */
+export function getAuthErrorMessage(errorCode: string, returnTitle: boolean = false): string {
+ if (!errorCode) {
+ const fallback = AUTH_ERROR_MESSAGES.UNKNOWN_ERROR;
+ return returnTitle ? fallback.title : fallback.description || fallback.title;
+ }
+
+ // Clean up the error code
+ const cleanErrorCode = errorCode.trim().toUpperCase();
+
+ // Try exact match first
+ let errorInfo = AUTH_ERROR_MESSAGES[cleanErrorCode] || AUTH_ERROR_MESSAGES[errorCode];
+
+ // Try partial matches for HTTP status codes
+ if (!errorInfo) {
+ const statusCodeMatch = errorCode.match(/(\d{3})/);
+ if (statusCodeMatch) {
+ errorInfo = AUTH_ERROR_MESSAGES[statusCodeMatch[1]];
+ }
+ }
+
+ // Try partial matches for common error patterns
+ if (!errorInfo) {
+ const patterns = [
+ { pattern: /credential|password|email/i, code: "LOGIN_BAD_CREDENTIALS" },
+ { pattern: /verify|verification/i, code: "LOGIN_USER_NOT_VERIFIED" },
+ { pattern: /inactive|disabled|suspended/i, code: "USER_INACTIVE" },
+ { pattern: /exists|duplicate/i, code: "REGISTER_USER_ALREADY_EXISTS" },
+ { pattern: /network|connection/i, code: "NETWORK_ERROR" },
+ { pattern: /timeout/i, code: "TIMEOUT" },
+ { pattern: /rate|limit|many/i, code: "429" },
+ ];
+
+ for (const { pattern, code } of patterns) {
+ if (pattern.test(errorCode)) {
+ errorInfo = AUTH_ERROR_MESSAGES[code];
+ break;
+ }
+ }
+ }
+
+ // Fallback to unknown error
+ if (!errorInfo) {
+ errorInfo = AUTH_ERROR_MESSAGES.UNKNOWN_ERROR;
+ }
+
+ return returnTitle ? errorInfo.title : errorInfo.description || errorInfo.title;
+}
+
+/**
+ * Get both title and description for an error
+ * @param errorCode - The error code or message from the API
+ * @returns Object with title and description
+ */
+export function getAuthErrorDetails(errorCode: string): { title: string; description: string } {
+ const title = getAuthErrorMessage(errorCode, true);
+ const description = getAuthErrorMessage(errorCode, false);
+
+ return { title, description };
+}
+
+/**
+ * Check if an error is a network-related error
+ * @param error - The error object or message
+ * @returns True if it's a network error
+ */
+export function isNetworkError(error: unknown): boolean {
+ if (error instanceof TypeError && error.message.includes('fetch')) {
+ return true;
+ }
+
+ if (typeof error === 'string') {
+ return /network|connection|fetch|cors/i.test(error);
+ }
+
+ return false;
+}
+
+/**
+ * Check if an error should trigger a retry action
+ * @param errorCode - The error code or message
+ * @returns True if retry is recommended
+ */
+export function shouldRetry(errorCode: string): boolean {
+ const retryableCodes = ['500', '503', '429', 'NETWORK_ERROR', 'TIMEOUT', 'server_error', 'temporarily_unavailable'];
+
+ return retryableCodes.some(code =>
+ errorCode.includes(code) ||
+ errorCode.toUpperCase().includes(code)
+ );
+}
From c2897d7fbecc07ef0da4772161c09a1bb24e7500 Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Thu, 18 Sep 2025 17:46:51 -0700
Subject: [PATCH 03/18] fix: linting and document limit
---
.../app/routes/documents_routes.py | 2 +-
surfsense_web/app/login/LocalLoginForm.tsx | 21 +-
surfsense_web/app/login/page.tsx | 18 +-
surfsense_web/app/register/page.tsx | 21 +-
surfsense_web/lib/auth-errors.ts | 325 +++++++++---------
5 files changed, 193 insertions(+), 194 deletions(-)
diff --git a/surfsense_backend/app/routes/documents_routes.py b/surfsense_backend/app/routes/documents_routes.py
index b75c56663..3ea1e8e61 100644
--- a/surfsense_backend/app/routes/documents_routes.py
+++ b/surfsense_backend/app/routes/documents_routes.py
@@ -157,7 +157,7 @@ async def create_documents_file_upload(
@router.get("/documents/", response_model=list[DocumentRead])
async def read_documents(
skip: int = 0,
- limit: int = 3000,
+ limit: int = 300,
search_space_id: int | None = None,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
diff --git a/surfsense_web/app/login/LocalLoginForm.tsx b/surfsense_web/app/login/LocalLoginForm.tsx
index 2a4d9239a..b464bea98 100644
--- a/surfsense_web/app/login/LocalLoginForm.tsx
+++ b/surfsense_web/app/login/LocalLoginForm.tsx
@@ -1,10 +1,10 @@
"use client";
+import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
-import { motion, AnimatePresence } from "framer-motion";
import { toast } from "sonner";
-import { getAuthErrorDetails, shouldRetry, isNetworkError } from "@/lib/auth-errors";
+import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
export function LocalLoginForm() {
const [username, setUsername] = useState("");
@@ -64,31 +64,30 @@ export function LocalLoginForm() {
setTimeout(() => {
router.push(`/auth/callback?token=${data.access_token}`);
}, 500);
-
} catch (err) {
// Use auth-errors utility to get proper error details
let errorCode = "UNKNOWN_ERROR";
-
+
if (err instanceof Error) {
errorCode = err.message;
} else if (isNetworkError(err)) {
errorCode = "NETWORK_ERROR";
}
-
+
// Get detailed error information from auth-errors utility
const errorDetails = getAuthErrorDetails(errorCode);
-
+
// Set persistent error display
setErrorTitle(errorDetails.title);
setError(errorDetails.description);
-
+
// Show error toast with conditional retry action
const toastOptions: any = {
id: loadingToast,
description: errorDetails.description,
duration: 6000,
};
-
+
// Add retry action if the error is retryable
if (shouldRetry(errorCode)) {
toastOptions.action = {
@@ -96,7 +95,7 @@ export function LocalLoginForm() {
onClick: () => handleSubmit(e),
};
}
-
+
toast.error(errorDetails.title, toastOptions);
} finally {
setIsLoading(false);
@@ -136,9 +135,7 @@ export function LocalLoginForm() {
{errorTitle}
-
- {error}
-
+
{error}
+
+
+
Contact Us
+
{
duration: 0.2,
},
}}
+ className="flex items-center gap-2"
>
+
+
+
+ Contact Us
+
+
{
{navItem.name}
))}
+ setOpen(false)}>
+
+
+ Contact Us
+
+
(null);
const isInView = useInView(ref);
- const { state: sidebarState } = useSidebar();
const [{ shouldShowHighlight, layoutStable }, dispatch] = useReducer(
highlightReducer,
initialState
diff --git a/surfsense_web/components/contact/contact-form.tsx b/surfsense_web/components/contact/contact-form.tsx
index 34d7e6311..4a658d302 100644
--- a/surfsense_web/components/contact/contact-form.tsx
+++ b/surfsense_web/components/contact/contact-form.tsx
@@ -1,371 +1,358 @@
"use client";
-import React, { useState } from "react";
-import { IconMailFilled } from "@tabler/icons-react";
-import { useId } from "react";
-import { cn } from "@/lib/utils";
-import Image from "next/image";
-import { motion } from "motion/react";
-import Link from "next/link";
-import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
+import { IconMailFilled } from "@tabler/icons-react";
+import { motion } from "motion/react";
+import Image from "next/image";
+import Link from "next/link";
+import type React from "react";
+import { useId, useState } from "react";
+import { useForm } from "react-hook-form";
import { toast } from "sonner";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
// Define validation schema matching the database schema
const contactFormSchema = z.object({
- name: z.string().min(1, 'Name is required').max(255, 'Name is too long'),
- email: z.string().email('Invalid email address').max(255, 'Email is too long'),
- company: z.string().min(1, 'Company is required').max(255, 'Company name is too long'),
- message: z.string().optional().default(''),
+ name: z.string().min(1, "Name is required").max(255, "Name is too long"),
+ email: z.string().email("Invalid email address").max(255, "Email is too long"),
+ company: z.string().min(1, "Company is required").max(255, "Company name is too long"),
+ message: z.string().optional().default(""),
});
type ContactFormData = z.infer;
export function ContactFormGridWithDetails() {
- const [isSubmitting, setIsSubmitting] = useState(false);
-
- const {
- register,
- handleSubmit,
- formState: { errors },
- reset,
- } = useForm({
- resolver: zodResolver(contactFormSchema),
- });
+ const [isSubmitting, setIsSubmitting] = useState(false);
- const onSubmit = async (data: ContactFormData) => {
- setIsSubmitting(true);
-
- try {
- const response = await fetch('/api/contact', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(data),
- });
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ reset,
+ } = useForm({
+ resolver: zodResolver(contactFormSchema),
+ });
- const result = await response.json();
+ const onSubmit = async (data: ContactFormData) => {
+ setIsSubmitting(true);
- if (response.ok) {
- toast.success('Message sent successfully!', {
- description: 'We will get back to you as soon as possible.',
- });
- reset();
- } else {
- toast.error('Failed to send message', {
- description: result.message || 'Please try again later.',
- });
- }
- } catch (error) {
- console.error('Error submitting form:', error);
- toast.error('Something went wrong', {
- description: 'Please try again later.',
- });
- } finally {
- setIsSubmitting(false);
- }
- };
+ try {
+ const response = await fetch("/api/contact", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(data),
+ });
- return (
-
-
-
-
-
-
-
-
- Contact us
-
-
- We are always looking for ways to improve our products and services.
- Contact us and let us know how we can help you.
-
+ const result = await response.json();
-
-
- rohan@surfsense.com
-
-
+ if (response.ok) {
+ toast.success("Message sent successfully!", {
+ description: "We will get back to you as soon as possible.",
+ });
+ reset();
+ } else {
+ toast.error("Failed to send message", {
+ description: result.message || "Please try again later.",
+ });
+ }
+ } catch (error) {
+ console.error("Error submitting form:", error);
+ toast.error("Something went wrong", {
+ description: "Please try again later.",
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
-
- https://cal.com/mod-surfsense
-
-
-
-
+ return (
+
+
+
+
+
+
+
+
+ Contact us
+
+
+ We are always looking for ways to improve our products and services. Contact us and let us
+ know how we can help you.
+
-
-
-
-
-
- );
+
+
+ rohan@surfsense.com
+
+
+
+
+ https://cal.com/mod-surfsense
+
+
+
+
+
+
+ );
}
const Pin = ({ className }: { className?: string }) => {
- return (
-
-
-
- We are here
-
-
+ return (
+
+
+
+ We are here
+
+
-
- <>
-
-
-
- >
-
+
+ <>
+
+
+
+ >
+
- <>
-
-
-
-
- >
-
-
- );
+ <>
+
+
+
+
+ >
+
+
+ );
};
export const FeatureIconContainer = ({
- children,
- className,
+ children,
+ className,
}: {
- children: React.ReactNode;
- className?: string;
+ children: React.ReactNode;
+ className?: string;
}) => {
- return (
-
- );
+ return (
+
+ );
};
-export const Grid = ({
- pattern,
- size,
-}: {
- pattern?: number[][];
- size?: number;
-}) => {
- const p = pattern ?? [
- [9, 3],
- [8, 5],
- [10, 2],
- [7, 4],
- [9, 6],
- ];
- return (
-
- );
+export const Grid = ({ pattern, size }: { pattern?: number[][]; size?: number }) => {
+ const p = pattern ?? [
+ [9, 3],
+ [8, 5],
+ [10, 2],
+ [7, 4],
+ [9, 6],
+ ];
+ return (
+
+ );
};
export function GridPattern({ width, height, x, y, squares, ...props }: any) {
- const patternId = useId();
+ const patternId = useId();
- return (
-
-
-
-
-
-
-
- {squares && (
-
- {squares.map(([x, y]: any, idx: number) => (
-
- ))}
-
- )}
-
- );
+ return (
+
+
+
+
+
+
+
+ {squares && (
+
+ {squares.map(([x, y]: any, idx: number) => (
+
+ ))}
+
+ )}
+
+ );
}
diff --git a/surfsense_web/components/onboard/add-provider-step.tsx b/surfsense_web/components/onboard/add-provider-step.tsx
index fc45df875..6a0983c03 100644
--- a/surfsense_web/components/onboard/add-provider-step.tsx
+++ b/surfsense_web/components/onboard/add-provider-step.tsx
@@ -1,7 +1,7 @@
"use client";
-import { motion } from "motion/react";
import { AlertCircle, Bot, Plus, Trash2 } from "lucide-react";
+import { motion } from "motion/react";
import { useState } from "react";
import { toast } from "sonner";
import { Alert, AlertDescription } from "@/components/ui/alert";
diff --git a/surfsense_web/components/onboard/assign-roles-step.tsx b/surfsense_web/components/onboard/assign-roles-step.tsx
index 1e4aa2ae2..016e8d6cc 100644
--- a/surfsense_web/components/onboard/assign-roles-step.tsx
+++ b/surfsense_web/components/onboard/assign-roles-step.tsx
@@ -1,7 +1,7 @@
"use client";
-import { motion } from "motion/react";
import { AlertCircle, Bot, Brain, CheckCircle, Zap } from "lucide-react";
+import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
diff --git a/surfsense_web/components/onboard/completion-step.tsx b/surfsense_web/components/onboard/completion-step.tsx
index 46787030b..e5dced0a8 100644
--- a/surfsense_web/components/onboard/completion-step.tsx
+++ b/surfsense_web/components/onboard/completion-step.tsx
@@ -1,7 +1,7 @@
"use client";
-import { motion } from "motion/react";
import { ArrowRight, Bot, Brain, CheckCircle, Sparkles, Zap } from "lucide-react";
+import { motion } from "motion/react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs";
diff --git a/surfsense_web/components/pricing.tsx b/surfsense_web/components/pricing.tsx
index becb13c96..3d19e1ea9 100644
--- a/surfsense_web/components/pricing.tsx
+++ b/surfsense_web/components/pricing.tsx
@@ -1,212 +1,202 @@
"use client";
+import NumberFlow from "@number-flow/react";
+import confetti from "canvas-confetti";
+import { motion } from "framer-motion";
+import { Check, Star } from "lucide-react";
+import Link from "next/link";
+import { useRef, useState } from "react";
import { buttonVariants } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useMediaQuery } from "@/hooks/use-media-query";
import { cn } from "@/lib/utils";
-import { motion } from "framer-motion";
-import { Check, Star } from "lucide-react";
-import Link from "next/link";
-import { useState, useRef } from "react";
-import NumberFlow from "@number-flow/react";
-import confetti from "canvas-confetti";
interface PricingPlan {
- name: string;
- price: string;
- yearlyPrice: string;
- period: string;
- features: string[];
- description: string;
- buttonText: string;
- href: string;
- isPopular: boolean;
+ name: string;
+ price: string;
+ yearlyPrice: string;
+ period: string;
+ features: string[];
+ description: string;
+ buttonText: string;
+ href: string;
+ isPopular: boolean;
}
interface PricingProps {
- plans: PricingPlan[];
- title?: string;
- description?: string;
+ plans: PricingPlan[];
+ title?: string;
+ description?: string;
}
export function Pricing({
- plans,
- title = "Simple, Transparent Pricing",
- description = "Choose the plan that works for you\nAll plans include access to our platform, lead generation tools, and dedicated support.",
+ plans,
+ title = "Simple, Transparent Pricing",
+ description = "Choose the plan that works for you\nAll plans include access to our platform, lead generation tools, and dedicated support.",
}: PricingProps) {
- const [isMonthly, setIsMonthly] = useState(true);
- const isDesktop = useMediaQuery("(min-width: 768px)");
- const switchRef = useRef(null);
+ const [isMonthly, setIsMonthly] = useState(true);
+ const isDesktop = useMediaQuery("(min-width: 768px)");
+ const switchRef = useRef(null);
- const handleToggle = (checked: boolean) => {
- setIsMonthly(!checked);
- if (checked && switchRef.current) {
- const rect = switchRef.current.getBoundingClientRect();
- const x = rect.left + rect.width / 2;
- const y = rect.top + rect.height / 2;
+ const handleToggle = (checked: boolean) => {
+ setIsMonthly(!checked);
+ if (checked && switchRef.current) {
+ const rect = switchRef.current.getBoundingClientRect();
+ const x = rect.left + rect.width / 2;
+ const y = rect.top + rect.height / 2;
- confetti({
- particleCount: 50,
- spread: 60,
- origin: {
- x: x / window.innerWidth,
- y: y / window.innerHeight,
- },
- colors: [
- "hsl(var(--primary))",
- "hsl(var(--accent))",
- "hsl(var(--secondary))",
- "hsl(var(--muted))",
- ],
- ticks: 200,
- gravity: 1.2,
- decay: 0.94,
- startVelocity: 30,
- shapes: ["circle"],
- });
- }
- };
+ confetti({
+ particleCount: 50,
+ spread: 60,
+ origin: {
+ x: x / window.innerWidth,
+ y: y / window.innerHeight,
+ },
+ colors: [
+ "hsl(var(--primary))",
+ "hsl(var(--accent))",
+ "hsl(var(--secondary))",
+ "hsl(var(--muted))",
+ ],
+ ticks: 200,
+ gravity: 1.2,
+ decay: 0.94,
+ startVelocity: 30,
+ shapes: ["circle"],
+ });
+ }
+ };
- return (
-
-
-
- {title}
-
-
- {description}
-
-
+ return (
+
+
+
{title}
+
{description}
+
-
-
-
- Annual billing (Save 20%)
-
-
+
+
+
+
+
+
+
+ Annual billing (Save 20%)
+
+
-
- {plans.map((plan, index) => (
-
- {plan.isPopular && (
-
-
-
- Popular
-
-
- )}
-
-
- {plan.name}
-
-
-
-
-
- {plan.period !== "Next 3 months" && (
-
- / {plan.period}
-
- )}
-
+
+ {plans.map((plan, index) => (
+
+ {plan.isPopular && (
+
+
+
+ Popular
+
+
+ )}
+
+
{plan.name}
+
+
+
+
+ {plan.period !== "Next 3 months" && (
+
+ / {plan.period}
+
+ )}
+
-
- {isMonthly ? "billed monthly" : "billed annually"}
-
+
+ {isMonthly ? "billed monthly" : "billed annually"}
+
-
- {plan.features.map((feature, idx) => (
- -
-
- {feature}
-
- ))}
-
+
+ {plan.features.map((feature, idx) => (
+ -
+
+ {feature}
+
+ ))}
+
-
+
-
- {plan.buttonText}
-
-
- {plan.description}
-
-
-
- ))}
-
-
- );
+
+ {plan.buttonText}
+
+ {plan.description}
+
+
+ ))}
+
+
+ );
}
diff --git a/surfsense_web/components/pricing/pricing-section.tsx b/surfsense_web/components/pricing/pricing-section.tsx
index 85c12bcd0..c017eb1e7 100644
--- a/surfsense_web/components/pricing/pricing-section.tsx
+++ b/surfsense_web/components/pricing/pricing-section.tsx
@@ -3,72 +3,72 @@
import { Pricing } from "@/components/pricing";
const demoPlans = [
- {
- name: "STARTER",
- price: "50",
- yearlyPrice: "40",
- period: "per month",
- features: [
- "Up to 10 projects",
- "Basic analytics",
- "48-hour support response time",
- "Limited API access",
- "Community support",
- ],
- description: "Perfect for individuals and small projects",
- buttonText: "Start Free Trial",
- href: "/sign-up",
- isPopular: false,
- },
- {
- name: "PROFESSIONAL",
- price: "99",
- yearlyPrice: "79",
- period: "per month",
- features: [
- "Unlimited projects",
- "Advanced analytics",
- "24-hour support response time",
- "Full API access",
- "Priority support",
- "Team collaboration",
- "Custom integrations",
- ],
- description: "Ideal for growing teams and businesses",
- buttonText: "Get Started",
- href: "/sign-up",
- isPopular: true,
- },
- {
- name: "ENTERPRISE",
- price: "299",
- yearlyPrice: "239",
- period: "per month",
- features: [
- "Everything in Professional",
- "Custom solutions",
- "Dedicated account manager",
- "1-hour support response time",
- "SSO Authentication",
- "Advanced security",
- "Custom contracts",
- "SLA agreement",
- ],
- description: "For large organizations with specific needs",
- buttonText: "Contact Sales",
- href: "/contact",
- isPopular: false,
- },
+ {
+ name: "STARTER",
+ price: "50",
+ yearlyPrice: "40",
+ period: "per month",
+ features: [
+ "Up to 10 projects",
+ "Basic analytics",
+ "48-hour support response time",
+ "Limited API access",
+ "Community support",
+ ],
+ description: "Perfect for individuals and small projects",
+ buttonText: "Start Free Trial",
+ href: "/sign-up",
+ isPopular: false,
+ },
+ {
+ name: "PROFESSIONAL",
+ price: "99",
+ yearlyPrice: "79",
+ period: "per month",
+ features: [
+ "Unlimited projects",
+ "Advanced analytics",
+ "24-hour support response time",
+ "Full API access",
+ "Priority support",
+ "Team collaboration",
+ "Custom integrations",
+ ],
+ description: "Ideal for growing teams and businesses",
+ buttonText: "Get Started",
+ href: "/sign-up",
+ isPopular: true,
+ },
+ {
+ name: "ENTERPRISE",
+ price: "299",
+ yearlyPrice: "239",
+ period: "per month",
+ features: [
+ "Everything in Professional",
+ "Custom solutions",
+ "Dedicated account manager",
+ "1-hour support response time",
+ "SSO Authentication",
+ "Advanced security",
+ "Custom contracts",
+ "SLA agreement",
+ ],
+ description: "For large organizations with specific needs",
+ buttonText: "Contact Sales",
+ href: "/contact",
+ isPopular: false,
+ },
];
function PricingBasic() {
- return (
-
- );
+ return (
+
+ );
}
export default PricingBasic;
diff --git a/surfsense_web/components/search-space-form.tsx b/surfsense_web/components/search-space-form.tsx
index d87461f5b..79102dbcf 100644
--- a/surfsense_web/components/search-space-form.tsx
+++ b/surfsense_web/components/search-space-form.tsx
@@ -1,8 +1,8 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { motion, type Variants } from "motion/react";
import { MoveLeftIcon, Plus, Search, Trash2 } from "lucide-react";
+import { motion, type Variants } from "motion/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx
index 3b23c6fc5..4107ee666 100644
--- a/surfsense_web/components/settings/llm-role-manager.tsx
+++ b/surfsense_web/components/settings/llm-role-manager.tsx
@@ -1,6 +1,5 @@
"use client";
-import { motion } from "motion/react";
import {
AlertCircle,
Bot,
@@ -13,6 +12,7 @@ import {
Settings2,
Zap,
} from "lucide-react";
+import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Alert, AlertDescription } from "@/components/ui/alert";
diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx
index c6ad7be1f..54bbd7990 100644
--- a/surfsense_web/components/settings/model-config-manager.tsx
+++ b/surfsense_web/components/settings/model-config-manager.tsx
@@ -1,6 +1,5 @@
"use client";
-import { AnimatePresence, motion } from "motion/react";
import {
AlertCircle,
Bot,
@@ -15,6 +14,7 @@ import {
Settings2,
Trash2,
} from "lucide-react";
+import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Alert, AlertDescription } from "@/components/ui/alert";
diff --git a/surfsense_web/components/theme/theme-toggle.tsx b/surfsense_web/components/theme/theme-toggle.tsx
index a2c807907..112cc7b88 100644
--- a/surfsense_web/components/theme/theme-toggle.tsx
+++ b/surfsense_web/components/theme/theme-toggle.tsx
@@ -1,7 +1,7 @@
"use client";
-import { motion } from "motion/react";
import { MoonIcon, SunIcon } from "lucide-react";
+import { motion } from "motion/react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
diff --git a/surfsense_web/components/ui/switch.tsx b/surfsense_web/components/ui/switch.tsx
index 8c464e46b..b64b32b73 100644
--- a/surfsense_web/components/ui/switch.tsx
+++ b/surfsense_web/components/ui/switch.tsx
@@ -1,29 +1,29 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as SwitchPrimitives from "@radix-ui/react-switch"
+import * as SwitchPrimitives from "@radix-ui/react-switch";
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-
-
-))
-Switch.displayName = SwitchPrimitives.Root.displayName
+
+
+
+));
+Switch.displayName = SwitchPrimitives.Root.displayName;
-export { Switch }
+export { Switch };
From 1a1530957a52ea472bf167cc3db013012ff64a9f Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Tue, 30 Sep 2025 22:15:55 -0700
Subject: [PATCH 15/18] feat: contact to frontpage and biome
---
surfsense_web/app/api/contact/route.ts | 106 +++++++++---------
surfsense_web/app/db/index.ts | 8 +-
surfsense_web/app/db/schema.ts | 10 +-
.../components/contact/contact-form.tsx | 5 +-
4 files changed, 65 insertions(+), 64 deletions(-)
diff --git a/surfsense_web/app/api/contact/route.ts b/surfsense_web/app/api/contact/route.ts
index c7cafa467..0af47dfe3 100644
--- a/surfsense_web/app/api/contact/route.ts
+++ b/surfsense_web/app/api/contact/route.ts
@@ -1,59 +1,61 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { db } from '@/app/db';
-import { usersTable } from '@/app/db/schema';
-import { z } from 'zod';
+import { type NextRequest, NextResponse } from "next/server";
+import { z } from "zod";
+import { db } from "@/app/db";
+import { usersTable } from "@/app/db/schema";
// Define validation schema matching the database schema
const contactSchema = z.object({
- name: z.string().min(1, 'Name is required').max(255, 'Name is too long'),
- email: z.string().email('Invalid email address').max(255, 'Email is too long'),
- company: z.string().min(1, 'Company is required').max(255, 'Company name is too long'),
- message: z.string().optional().default(''),
+ name: z.string().min(1, "Name is required").max(255, "Name is too long"),
+ email: z.string().email("Invalid email address").max(255, "Email is too long"),
+ company: z.string().min(1, "Company is required").max(255, "Company name is too long"),
+ message: z.string().optional().default(""),
});
export async function POST(request: NextRequest) {
- try {
- const body = await request.json();
-
- // Validate the request body
- const validatedData = contactSchema.parse(body);
-
- // Insert into database
- const result = await db.insert(usersTable).values({
- name: validatedData.name,
- email: validatedData.email,
- company: validatedData.company,
- message: validatedData.message,
- }).returning();
-
- return NextResponse.json(
- {
- success: true,
- message: 'Contact form submitted successfully',
- data: result[0]
- },
- { status: 201 }
- );
- } catch (error) {
- if (error instanceof z.ZodError) {
- return NextResponse.json(
- {
- success: false,
- message: 'Validation error',
- errors: error.errors
- },
- { status: 400 }
- );
- }
-
- console.error('Error submitting contact form:', error);
- return NextResponse.json(
- {
- success: false,
- message: 'Failed to submit contact form'
- },
- { status: 500 }
- );
- }
-}
+ try {
+ const body = await request.json();
+ // Validate the request body
+ const validatedData = contactSchema.parse(body);
+
+ // Insert into database
+ const result = await db
+ .insert(usersTable)
+ .values({
+ name: validatedData.name,
+ email: validatedData.email,
+ company: validatedData.company,
+ message: validatedData.message,
+ })
+ .returning();
+
+ return NextResponse.json(
+ {
+ success: true,
+ message: "Contact form submitted successfully",
+ data: result[0],
+ },
+ { status: 201 }
+ );
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ {
+ success: false,
+ message: "Validation error",
+ errors: error.errors,
+ },
+ { status: 400 }
+ );
+ }
+
+ console.error("Error submitting contact form:", error);
+ return NextResponse.json(
+ {
+ success: false,
+ message: "Failed to submit contact form",
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/surfsense_web/app/db/index.ts b/surfsense_web/app/db/index.ts
index 5d7a87757..277848bb5 100644
--- a/surfsense_web/app/db/index.ts
+++ b/surfsense_web/app/db/index.ts
@@ -1,6 +1,6 @@
-import { drizzle } from 'drizzle-orm/postgres-js'
-import postgres from 'postgres'
-import * as schema from './schema'
+import { drizzle } from "drizzle-orm/postgres-js";
+import postgres from "postgres";
+import * as schema from "./schema";
-const client = postgres(process.env.DATABASE_URL!)
+const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle({ client, schema });
diff --git a/surfsense_web/app/db/schema.ts b/surfsense_web/app/db/schema.ts
index fc5678bc3..d8b941ba5 100644
--- a/surfsense_web/app/db/schema.ts
+++ b/surfsense_web/app/db/schema.ts
@@ -1,9 +1,9 @@
import { integer, pgTable, text, varchar } from "drizzle-orm/pg-core";
export const usersTable = pgTable("users", {
- id: integer().primaryKey().generatedAlwaysAsIdentity(),
- name: varchar({ length: 255 }).notNull(),
- email: varchar({ length: 255 }).notNull().unique(),
- company: varchar({ length: 255 }).notNull(),
- message: text().default(''),
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
+ name: varchar({ length: 255 }).notNull(),
+ email: varchar({ length: 255 }).notNull().unique(),
+ company: varchar({ length: 255 }).notNull(),
+ message: text().default(""),
});
diff --git a/surfsense_web/components/contact/contact-form.tsx b/surfsense_web/components/contact/contact-form.tsx
index 4a658d302..0fd07a056 100644
--- a/surfsense_web/components/contact/contact-form.tsx
+++ b/surfsense_web/components/contact/contact-form.tsx
@@ -76,11 +76,10 @@ export function ContactFormGridWithDetails() {
- Contact us
+ Contact
- We are always looking for ways to improve our products and services. Contact us and let us
- know how we can help you.
+ We'd love to Hear From You.
From 79429b0c9a34fe36cbb36357e82134e2224897ce Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Tue, 30 Sep 2025 22:26:32 -0700
Subject: [PATCH 16/18] remove framer-motion
---
surfsense_web/components/pricing.tsx | 2 +-
surfsense_web/package.json | 1 -
surfsense_web/pnpm-lock.yaml | 3 ---
3 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/surfsense_web/components/pricing.tsx b/surfsense_web/components/pricing.tsx
index 3d19e1ea9..b666c8982 100644
--- a/surfsense_web/components/pricing.tsx
+++ b/surfsense_web/components/pricing.tsx
@@ -2,7 +2,7 @@
import NumberFlow from "@number-flow/react";
import confetti from "canvas-confetti";
-import { motion } from "framer-motion";
+import { motion } from "motion/react";
import { Check, Star } from "lucide-react";
import Link from "next/link";
import { useRef, useState } from "react";
diff --git a/surfsense_web/package.json b/surfsense_web/package.json
index b17dfd626..517e7690d 100644
--- a/surfsense_web/package.json
+++ b/surfsense_web/package.json
@@ -54,7 +54,6 @@
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.5",
"emblor": "^1.4.8",
- "framer-motion": "^12.23.22",
"fumadocs-core": "^15.6.6",
"fumadocs-mdx": "^11.7.1",
"fumadocs-ui": "^15.6.6",
diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml
index a62cd0a53..bfda2d29d 100644
--- a/surfsense_web/pnpm-lock.yaml
+++ b/surfsense_web/pnpm-lock.yaml
@@ -113,9 +113,6 @@ importers:
emblor:
specifier: ^1.4.8
version: 1.4.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
- framer-motion:
- specifier: ^12.23.22
- version: 12.23.22(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
fumadocs-core:
specifier: ^15.6.6
version: 15.6.6(@types/react@19.1.8)(next@15.4.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
From e274c1307c798b49adb9bf1f9731e82ef65cb525 Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Tue, 30 Sep 2025 22:45:04 -0700
Subject: [PATCH 17/18] try vercel fix
---
surfsense_web/app/db/index.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/surfsense_web/app/db/index.ts b/surfsense_web/app/db/index.ts
index 277848bb5..db86725e1 100644
--- a/surfsense_web/app/db/index.ts
+++ b/surfsense_web/app/db/index.ts
@@ -2,5 +2,12 @@ import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
-const client = postgres(process.env.DATABASE_URL!);
+// Configure postgres client for Vercel serverless environment
+const client = postgres(process.env.DATABASE_URL!, {
+ max: 1, // Limit connections for serverless (Vercel)
+ idle_timeout: 20, // Close idle connections after 20 seconds
+ max_lifetime: 60 * 30, // Close connections after 30 minutes
+ connect_timeout: 10, // Connection timeout in seconds
+});
+
export const db = drizzle({ client, schema });
From 63b2c2f0a097117cca7c5ef776291dbb4c03f31c Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Tue, 30 Sep 2025 23:05:12 -0700
Subject: [PATCH 18/18] fix: sitemap
---
surfsense_web/app/sitemap.ts | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/surfsense_web/app/sitemap.ts b/surfsense_web/app/sitemap.ts
index 1380fc561..70fb45491 100644
--- a/surfsense_web/app/sitemap.ts
+++ b/surfsense_web/app/sitemap.ts
@@ -3,43 +3,49 @@ import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
- url: "https://www.surfsense.net/",
+ url: "https://www.surfsense.com/",
lastModified: new Date(),
changeFrequency: "yearly",
priority: 1,
},
{
- url: "https://www.surfsense.net/privacy",
+ url: "https://www.surfsense.com/contact",
+ lastModified: new Date(),
+ changeFrequency: "yearly",
+ priority: 1,
+ },
+ {
+ url: "https://www.surfsense.com/privacy",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/terms",
+ url: "https://www.surfsense.com/terms",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/docs",
+ url: "https://www.surfsense.com/docs",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/docs/installation",
+ url: "https://www.surfsense.com/docs/installation",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/docs/docker-installation",
+ url: "https://www.surfsense.com/docs/docker-installation",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
- url: "https://www.surfsense.net/docs/manual-installation",
+ url: "https://www.surfsense.com/docs/manual-installation",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,