Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@sendgrid/mail": "^8.1.6",
"@tanstack/react-query": "^5.85.5",
"@vercel/analytics": "^2.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
Expand Down
40 changes: 40 additions & 0 deletions src/app/api/consultations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
consultationApiSchema,
type ConsultationApiData,
} from "@/lib/validation";
import {
serverAnalyticsEvents,
trackServerAnalyticsEvent,
} from "@/lib/server-analytics";
import { z } from "zod";

export const runtime = "nodejs";
Expand Down Expand Up @@ -215,6 +219,15 @@ export async function POST(request: NextRequest) {
message: issue.message,
}));

void trackServerAnalyticsEvent(
serverAnalyticsEvents.consultationSubmitFailed,
{
reason: "validation",
service: "api",
},
request
);

return NextResponse.json(
{
success: false,
Expand Down Expand Up @@ -253,6 +266,15 @@ export async function POST(request: NextRequest) {
}))
);

void trackServerAnalyticsEvent(
serverAnalyticsEvents.consultationSubmitFailed,
{
reason: "notification",
service: failedNotifications.map(({ service }) => service).join(","),
},
request
);

return NextResponse.json(
{
success: false,
Expand All @@ -266,6 +288,15 @@ export async function POST(request: NextRequest) {
);
}

void trackServerAnalyticsEvent(
serverAnalyticsEvents.consultationSubmitted,
{
budget: summary.budget,
servicesCount: summary.services.length,
},
request
);

return NextResponse.json(
{
success: true,
Expand All @@ -281,6 +312,15 @@ export async function POST(request: NextRequest) {
} catch (error: any) {
console.error("Error submitting consultation:", error);

void trackServerAnalyticsEvent(
serverAnalyticsEvents.consultationSubmitFailed,
{
reason: "exception",
service: "api",
},
request
);

return NextResponse.json(
{
success: false,
Expand Down
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./globals.css";
// import { ThemeProvider } from "next-themes";
import { Providers } from "@/providers/providers";
import Fathom from "@/components/Fathom";
import VercelAnalytics from "@/components/VercelAnalytics";

export const metadata: Metadata = {
title: "Raid Guild",
Expand All @@ -21,6 +22,7 @@ export default function RootLayout({
className={`${maziusDisplay.variable} ${ebGaramond.variable} ${ubuntuMono.variable} antialiased`}
>
<Fathom />
<VercelAnalytics />
{/* <ThemeProvider
attribute="class"
defaultTheme="light"
Expand Down
85 changes: 81 additions & 4 deletions src/components/HireUs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import Image from "next/image";
import MultipleSelector from "./ui/multiselect";
import { DISCORD_INVITE_URL } from "@/lib/data/constants";
import { trackEvent } from "fathom-client";
import { analyticsEvents, trackAnalyticsEvent } from "@/lib/analytics";

interface StepProps {
form: ReturnType<typeof useForm<HireUsFormData>>;
Expand Down Expand Up @@ -346,6 +347,8 @@ export default function HireUs() {
const [validationErrors, setValidationErrors] = useState<
Array<{ field: string; message: string }>
>([]);
const formRef = React.useRef<HTMLDivElement>(null);
const hasTrackedFormView = React.useRef(false);

const form = useForm<HireUsFormData>({
resolver: zodResolver(hireUsFormSchema),
Expand All @@ -365,14 +368,45 @@ export default function HireUs() {
},
});

React.useEffect(() => {
if (!formRef.current || hasTrackedFormView.current) return;

const observer = new IntersectionObserver(
([entry]) => {
if (!entry?.isIntersecting || hasTrackedFormView.current) return;

hasTrackedFormView.current = true;
trackAnalyticsEvent(analyticsEvents.hireFormViewed);
observer.disconnect();
},
{ threshold: 0.35 }
);

observer.observe(formRef.current);

return () => observer.disconnect();
}, []);

// Validation functions for each step
const validatePersonalInfo = async () => {
const result = await form.trigger(["name", "email", "bio"]);
if (!result) {
trackAnalyticsEvent(analyticsEvents.hireFormSubmitError, {
reason: "validation",
step: "contact_info",
});
}
return result;
};

const validateProjectDetails = async () => {
const result = await form.trigger(["projectName", "description"]);
if (!result) {
trackAnalyticsEvent(analyticsEvents.hireFormSubmitError, {
reason: "validation",
step: "project_details",
});
}
return result;
};

Expand All @@ -383,6 +417,12 @@ export default function HireUs() {
"services",
"projectPriority",
]);
if (!result) {
trackAnalyticsEvent(analyticsEvents.hireFormSubmitError, {
reason: "validation",
step: "requirements",
});
}
return result;
};

Expand All @@ -391,6 +431,7 @@ export default function HireUs() {

const formData = form.getValues();
console.log("Wizard completed:", formData);
const servicesCount = formData.services?.length ?? 0;

// Reset states
setIsSubmitting(true);
Expand All @@ -400,6 +441,10 @@ export default function HireUs() {

//tracking
trackEvent("hire-us-submission");
trackAnalyticsEvent(analyticsEvents.hireFormSubmitAttempt, {
budget: formData.budget,
servicesCount,
});

// Transform form data to API format using the centralized function
const consultData = transformFormDataToApiFormat(formData);
Expand All @@ -421,21 +466,37 @@ export default function HireUs() {

//tracking
trackEvent("hire-us-submission");
trackAnalyticsEvent(analyticsEvents.hireFormSubmitSuccess, {
budget: formData.budget,
servicesCount,
});
// Reset form after successful submission
form.reset();
} else {
console.error("Failed to submit consultation:", result);
setSubmissionStatus("error");

// Handle validation errors
if (result.details && Array.isArray(result.details)) {
if (
response.status === 400 &&
result.details &&
Array.isArray(result.details)
) {
console.error("Validation errors:", result.details);
setValidationErrors(result.details);
setErrorMessage("Please fix the validation errors below.");
trackAnalyticsEvent(analyticsEvents.hireFormSubmitError, {
reason: "server_validation",
step: "submit",
});
} else {
setErrorMessage(
result.error || "Failed to submit consultation. Please try again."
);
trackAnalyticsEvent(analyticsEvents.hireFormSubmitError, {
reason: "server_error",
step: "submit",
});
}
}
} catch (error) {
Expand All @@ -444,6 +505,10 @@ export default function HireUs() {
setErrorMessage(
"Network error. Please check your connection and try again."
);
trackAnalyticsEvent(analyticsEvents.hireFormSubmitError, {
reason: "network",
step: "submit",
});
} finally {
setIsSubmitting(false);
}
Expand Down Expand Up @@ -527,15 +592,27 @@ export default function HireUs() {
// description: "Tell us about yourself",
component: <PersonalInfoStep form={form} />,
validation: validatePersonalInfo,
onStepComplete: () => trackEvent("hire-us-step-1"),
onStepComplete: () => {
trackEvent("hire-us-step-1");
trackAnalyticsEvent(analyticsEvents.hireFormStepCompleted, {
step: "contact_info",
stepNumber: 1,
});
},
},
{
id: "project-description",
title: "Project Description",
// description: "Describe your project requirements",
component: <ProjectDetailsStep form={form} />,
validation: validateProjectDetails,
onStepComplete: () => trackEvent("hire-us-step-2"),
onStepComplete: () => {
trackEvent("hire-us-step-2");
trackAnalyticsEvent(analyticsEvents.hireFormStepCompleted, {
step: "project_details",
stepNumber: 2,
});
},
},
{
id: "requirements",
Expand Down Expand Up @@ -567,7 +644,7 @@ export default function HireUs() {
</div>

{/* Wizard */}
<div className="space-y-4">
<div className="space-y-4" ref={formRef}>
<Form {...form}>
{submissionStatus === "success" ? (
<SuccessState />
Expand Down
16 changes: 15 additions & 1 deletion src/components/JoinUs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import {
FormField,
FormItem,
FormLabel,
RequiredFieldIndicator,
} from "@/components/ui/form";
import { joinUsFormSchema, type JoinUsFormData } from "@/lib/validation";
import Image from "next/image";
import { trackEvent } from "fathom-client";
import { Button } from "./ui/button";
import { analyticsEvents, trackAnalyticsEvent } from "@/lib/analytics";

const joinUsImages = [
"/images/join-image-1-bw.webp",
Expand Down Expand Up @@ -55,6 +55,9 @@ export default function JoinUs({ referral }: JoinUsProps) {
setIsSubmitting(true);
setSubmissionStatus("idle");
setErrorMessage("");
trackAnalyticsEvent(analyticsEvents.joinSignupAttempt, {
hasReferral: Boolean(referral),
});

try {
const response = await fetch("/api/email-referrals", {
Expand All @@ -76,19 +79,30 @@ export default function JoinUs({ referral }: JoinUsProps) {

//tracking
trackEvent("join-us-submission");
trackAnalyticsEvent(analyticsEvents.joinSignupSuccess, {
hasReferral: Boolean(referral),
});
// Reset form after successful submission
form.reset();
} else {
console.error("Failed to submit email referral:", result);
setSubmissionStatus("error");
setErrorMessage(result.error || "Failed to submit. Please try again.");
trackAnalyticsEvent(analyticsEvents.joinSignupError, {
hasReferral: Boolean(referral),
reason: "server_error",
});
}
} catch (error) {
console.error("Error submitting application:", error);
setSubmissionStatus("error");
setErrorMessage(
"Network error. Please check your connection and try again.",
);
trackAnalyticsEvent(analyticsEvents.joinSignupError, {
hasReferral: Boolean(referral),
reason: "network",
});
} finally {
setIsSubmitting(false);
}
Expand Down
Loading