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
2 changes: 1 addition & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ export default defineConfig({
},

integrations: [react()],
adapter: cloudflare({workerEntryPoint: {path: "./src/worker.ts"}})
adapter: cloudflare({ workerEntryPoint: { path: "./src/worker.ts" } })
});
7 changes: 2 additions & 5 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { login } from "./login";
import { logout } from "./logout";
import * as websiteActions from '../modules/website/website.actions'
import { verifyTrackingScript } from "./verify-tracking-script";
import * as websiteActions from '@/modules/website/website.actions'
import { login , logout } from "@/modules/auth/auth.actions";
import { dashboardActions } from "@/modules/dashboard/dashboard.actions";

export const server = {
login,
logout,
...websiteActions,
verifyTrackingScript,
dashboardActions
};
12 changes: 0 additions & 12 deletions src/actions/logout.ts

This file was deleted.

60 changes: 0 additions & 60 deletions src/actions/verify-tracking-script.ts

This file was deleted.

74 changes: 27 additions & 47 deletions src/hooks/use-action.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useTransition, useRef } from "react";
import { useState } from "react";
import { toast } from "sonner";

interface UseActionOptions<TData> {
Expand All @@ -13,61 +13,41 @@ type ActionResult<TData> = {
error?: { message?: string } | string | null;
};

export function useAction<TInput, TData>(
actionFn: (input: TInput) => Promise<ActionResult<TData>>,
options?: UseActionOptions<TData>
) {
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
export function useAction<TInput, TData>(actionFn: (input: TInput) => Promise<ActionResult<TData>>, options?: UseActionOptions<TData>) {

// prevents race conditions (very important for dashboard filters)
const requestIdRef = useRef(0);
const [isLoading, setIsLoading] = useState(false);

const execute = (input: TInput) => {
setError(null);
const execute = async (input: TInput) => {
const toastId = toast.loading(options?.loadingMessage ?? "Processing your request...");

const toastId = toast.loading(
options?.loadingMessage ?? "Processing your request..."
);
try {
setIsLoading(true)
const result = await actionFn(input);

const requestId = ++requestIdRef.current;

startTransition(async () => {
try {
const result = await actionFn(input);

// ignore stale responses
if (requestId !== requestIdRef.current) return;

if (result.error) {
const errMsg =
typeof result.error === "string"
? result.error
: result.error.message ?? "An unexpected error occurred.";

setError(errMsg);
options?.onError?.(errMsg);
toast.error(errMsg, { id: toastId });
return;
}

toast.success(options?.successMessage ?? "Success", {
id: toastId,
});

if (result.data) {
options?.onSuccess?.(result.data);
}
} catch (err) {
if (result.error) {
const errMsg =
err instanceof Error ? err.message : "Failed to execute action.";
typeof result.error === "string"
? result.error
: result.error.message ?? "An unexpected error occurred.";

setError(errMsg);
options?.onError?.(errMsg);
toast.error(errMsg, { id: toastId });
return;
}

toast.success(options?.successMessage ?? "Success", { id: toastId });

if (result.data) {
options?.onSuccess?.(result.data);
}
});
} catch (err) {
const errMsg = err instanceof Error ? err.message : "Failed to execute action.";
options?.onError?.(errMsg);
toast.error(errMsg, { id: toastId });
} finally {
setIsLoading(false)
}
};

return { execute, isLoading: isPending, error };
return { execute , isLoading};
}
48 changes: 48 additions & 0 deletions src/hooks/use-form-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, type UseFormReturn, type DefaultValues, type FieldValues } from "react-hook-form";
import { useAction } from "@/hooks/use-action";
import type { ZodType } from "zod";

interface UseFormActionOptions<TFieldValues extends FieldValues, TData> {
schema: ZodType<TFieldValues, any, any>;
defaultValues: DefaultValues<TFieldValues>;
actionFn: (input: TFieldValues) => Promise<{ data?: TData; error?: any }>;
loadingMessage?: string;
successMessage?: string;
onSuccess?: (data: TData) => void;
}

interface UseFormActionReturn<TFieldValues extends FieldValues> {
form: UseFormReturn<TFieldValues, any>; // Add 'any' context parameter to align return types
onSubmit: (data: TFieldValues) => void;
}

export function useFormAction<TFieldValues extends FieldValues, TData = any>({
schema,
defaultValues,
actionFn,
loadingMessage,
successMessage,
onSuccess,
}: UseFormActionOptions<TFieldValues, TData>): UseFormActionReturn<TFieldValues> {

const form = useForm<TFieldValues>({
resolver: zodResolver(schema) as any,
defaultValues,
});

const { execute } = useAction(actionFn, {
loadingMessage,
successMessage,
onSuccess: (data) => {
form.reset();
onSuccess?.(data);
},
});

const onSubmit = (data: TFieldValues) => {
execute(data);
};

return { form, onSubmit };
}
13 changes: 3 additions & 10 deletions src/hooks/use-log-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ export default function useLogIn() {
});

// Setup the action runner using your custom hook
const { execute, isLoading, error } = useAction(
const { execute } = useAction(
async (data: LoginInput) => {
const result = await actions.login(data);

// Astro actions return an ActionError object on failure.
// We pass it forward or shape it to match your ActionResult type.

return {
data: result.data,
error: result.error ? { message: result.error.message } : null,
Expand All @@ -47,10 +45,5 @@ export default function useLogIn() {
execute(data);
};

return {
form,
onSubmit,
isPending: isLoading, // Maps to your hook's isLoading state
error
};
return { form, onSubmit };
}
16 changes: 13 additions & 3 deletions src/actions/login.ts → src/modules/auth/auth.actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { loginSchema } from '@/schema/login';
import { defineAction } from 'astro:actions';

import { SignJWT } from 'jose';
import { loginSchema } from './auth.schema';

export const login = defineAction({
accept: 'json',
Expand Down Expand Up @@ -36,4 +35,15 @@ export const login = defineAction({

return { success: true };
},
})
})

export const logout = defineAction({
handler: async (_, context) => {
// Delete the authentication cookie
context.cookies.delete('auth_token', {
path: '/',
});

return { success: true };
},
});
30 changes: 30 additions & 0 deletions src/modules/auth/auth.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { z } from 'astro:schema';

// Single Source of Truth for Constants
export const AUTH_CONFIG = {
PASSWORD_MIN_LENGTH: 6,
PASSWORD_MAX_LENGTH: 50,
} as const;

// Single Source of Truth for Validation Messages
export const AUTH_MESSAGES = {
EMAIL_INVALID: "Please enter a valid business email address.",
PASSWORD_TOO_SHORT: `Password must be at least ${AUTH_CONFIG.PASSWORD_MIN_LENGTH} characters long.`,
PASSWORD_TOO_LONG: `Password cannot exceed ${AUTH_CONFIG.PASSWORD_MAX_LENGTH} characters.`,
} as const;

// User-Friendly Schema Configuration
export const loginSchema = z.object({
email: z
.string()
.trim()
.toLowerCase()
.email({ message: AUTH_MESSAGES.EMAIL_INVALID }),

password: z
.string()
.min(AUTH_CONFIG.PASSWORD_MIN_LENGTH, { message: AUTH_MESSAGES.PASSWORD_TOO_SHORT })
.max(AUTH_CONFIG.PASSWORD_MAX_LENGTH, { message: AUTH_MESSAGES.PASSWORD_TOO_LONG }),
});

export type LoginInput = z.infer<typeof loginSchema>;
8 changes: 4 additions & 4 deletions src/modules/dashboard/components/dashboard.header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { Button } from "@/components/ui/button";
import { useAction } from "@/hooks/use-action";
import { actions } from "astro:actions";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogCancel,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
AlertDialog,
} from "@/components/ui/alert-dialog";

interface DashboardHeaderProps {
Expand Down
2 changes: 1 addition & 1 deletion src/modules/website/components/website.manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function WebsiteManager({ initialWebsites, errorMsg }: WebsitePageProps)
});

// 4. Hook for Script Verification Action
const verifyAction = useAction(actions.verifyTrackingScript, {
const verifyAction = useAction(actions.verifyWebsiteTrackingScript, {
successMessage: "Tracking script verified successfully!",
loadingMessage: "Verifying tracking script placement...",
});
Expand Down
Loading
Loading