diff --git a/astro.config.mjs b/astro.config.mjs
index d937ef1..2a1c436 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -17,5 +17,5 @@ export default defineConfig({
},
integrations: [react()],
- adapter: cloudflare({workerEntryPoint: {path: "./src/worker.ts"}})
+ adapter: cloudflare({ workerEntryPoint: { path: "./src/worker.ts" } })
});
\ No newline at end of file
diff --git a/src/actions/index.ts b/src/actions/index.ts
index e142468..d6bac8a 100644
--- a/src/actions/index.ts
+++ b/src/actions/index.ts
@@ -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
};
\ No newline at end of file
diff --git a/src/actions/logout.ts b/src/actions/logout.ts
deleted file mode 100644
index a2dbf06..0000000
--- a/src/actions/logout.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineAction } from 'astro:actions';
-
-export const logout = defineAction({
- handler: async (_, context) => {
- // Delete the authentication cookie
- context.cookies.delete('auth_token', {
- path: '/',
- });
-
- return { success: true };
- },
-});
\ No newline at end of file
diff --git a/src/actions/verify-tracking-script.ts b/src/actions/verify-tracking-script.ts
deleted file mode 100644
index 85cde64..0000000
--- a/src/actions/verify-tracking-script.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { ActionError, defineAction } from "astro:actions";
-import { z } from "astro:schema";
-
-export const verifyTrackingScript = defineAction({
- accept: "json",
- input: z.object({
- url: z.string().url(),
- websiteId: z.string(),
- }),
- handler: async ({ url, websiteId }) => {
-
- const response = await fetch(url, {
- headers: {
- "User-Agent": "TrackFlow-Verification-Bot/1.0",
- },
- signal: AbortSignal.timeout(5000),
- });
-
- if (!response.ok) {
- throw new ActionError({
- code: "BAD_REQUEST",
- message: `Could not reach site. Server responded with status ${response.status}.`,
- });
- }
-
- const html = await response.text();
-
- const allowedDomains = [
- "localhost:3000",
- "tf.saifulalom.com",
- "trackflow.saifulalom.com",
- ]
-
- const hasScriptSrc = allowedDomains.some(domain => html.includes(`${domain}/script.js`));
- const hasWebsiteId = html.includes(`data-website-id="${websiteId}"`);
-
- const isVerified = hasScriptSrc && hasWebsiteId;
-
- if (isVerified) {
- return {
- success: isVerified,
- id: websiteId
- };
- }
-
- if (!hasScriptSrc) {
- throw new ActionError({
- code: "NOT_FOUND",
- message: "Tracking script tag could not be found. Please ensure the script tag is added to your website's
.",
- });
- }
-
- if (!hasWebsiteId) {
- throw new ActionError({
- code: "BAD_REQUEST",
- message: "Script detected, but the 'data-website-id' attribute is missing or incorrect for this website.",
- });
- }
- },
-});
\ No newline at end of file
diff --git a/src/hooks/use-action.ts b/src/hooks/use-action.ts
index 2d7e18d..8725c2b 100644
--- a/src/hooks/use-action.ts
+++ b/src/hooks/use-action.ts
@@ -1,4 +1,4 @@
-import { useState, useTransition, useRef } from "react";
+import { useState } from "react";
import { toast } from "sonner";
interface UseActionOptions {
@@ -13,61 +13,41 @@ type ActionResult = {
error?: { message?: string } | string | null;
};
-export function useAction(
- actionFn: (input: TInput) => Promise>,
- options?: UseActionOptions
-) {
- const [isPending, startTransition] = useTransition();
- const [error, setError] = useState(null);
+export function useAction(actionFn: (input: TInput) => Promise>, options?: UseActionOptions) {
- // 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};
}
\ No newline at end of file
diff --git a/src/hooks/use-form-action.ts b/src/hooks/use-form-action.ts
new file mode 100644
index 0000000..3586f75
--- /dev/null
+++ b/src/hooks/use-form-action.ts
@@ -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 {
+ schema: ZodType;
+ defaultValues: DefaultValues;
+ actionFn: (input: TFieldValues) => Promise<{ data?: TData; error?: any }>;
+ loadingMessage?: string;
+ successMessage?: string;
+ onSuccess?: (data: TData) => void;
+}
+
+interface UseFormActionReturn {
+ form: UseFormReturn; // Add 'any' context parameter to align return types
+ onSubmit: (data: TFieldValues) => void;
+}
+
+export function useFormAction({
+ schema,
+ defaultValues,
+ actionFn,
+ loadingMessage,
+ successMessage,
+ onSuccess,
+}: UseFormActionOptions): UseFormActionReturn {
+
+ const form = useForm({
+ 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 };
+}
\ No newline at end of file
diff --git a/src/hooks/use-log-in.ts b/src/hooks/use-log-in.ts
index 4b45deb..89ea51b 100644
--- a/src/hooks/use-log-in.ts
+++ b/src/hooks/use-log-in.ts
@@ -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,
@@ -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 };
}
\ No newline at end of file
diff --git a/src/actions/login.ts b/src/modules/auth/auth.actions.ts
similarity index 79%
rename from src/actions/login.ts
rename to src/modules/auth/auth.actions.ts
index 992936e..159e508 100644
--- a/src/actions/login.ts
+++ b/src/modules/auth/auth.actions.ts
@@ -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',
@@ -36,4 +35,15 @@ export const login = defineAction({
return { success: true };
},
-})
\ No newline at end of file
+})
+
+export const logout = defineAction({
+ handler: async (_, context) => {
+ // Delete the authentication cookie
+ context.cookies.delete('auth_token', {
+ path: '/',
+ });
+
+ return { success: true };
+ },
+});
\ No newline at end of file
diff --git a/src/modules/auth/auth.schema.ts b/src/modules/auth/auth.schema.ts
new file mode 100644
index 0000000..25575c6
--- /dev/null
+++ b/src/modules/auth/auth.schema.ts
@@ -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;
\ No newline at end of file
diff --git a/src/modules/dashboard/components/dashboard.header.tsx b/src/modules/dashboard/components/dashboard.header.tsx
index eb1740c..a78e3b0 100644
--- a/src/modules/dashboard/components/dashboard.header.tsx
+++ b/src/modules/dashboard/components/dashboard.header.tsx
@@ -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 {
diff --git a/src/modules/website/components/website.manager.tsx b/src/modules/website/components/website.manager.tsx
index 4d06ddc..f6da271 100644
--- a/src/modules/website/components/website.manager.tsx
+++ b/src/modules/website/components/website.manager.tsx
@@ -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...",
});
diff --git a/src/modules/website/website.actions.ts b/src/modules/website/website.actions.ts
index 8ffe83b..79449e2 100644
--- a/src/modules/website/website.actions.ts
+++ b/src/modules/website/website.actions.ts
@@ -62,4 +62,63 @@ export const deleteSite = defineAction({
const deletedSite = await siteRepository.delete(db, input.id);
return { success: true, data: deletedSite };
},
-})
\ No newline at end of file
+})
+
+
+export const verifyWebsiteTrackingScript = defineAction({
+ accept: "json",
+ input: z.object({
+ url: z.string().url(),
+ websiteId: z.string(),
+ }),
+ handler: async ({ url, websiteId }) => {
+
+ const response = await fetch(url, {
+ headers: {
+ "User-Agent": "TrackFlow-Verification-Bot/1.0",
+ },
+ signal: AbortSignal.timeout(5000),
+ });
+
+ if (!response.ok) {
+ throw new ActionError({
+ code: "BAD_REQUEST",
+ message: `Could not reach site. Server responded with status ${response.status}.`,
+ });
+ }
+
+ const html = await response.text();
+
+ const allowedDomains = [
+ "localhost:3000",
+ "tf.saifulalom.com",
+ "trackflow.saifulalom.com",
+ ]
+
+ const hasScriptSrc = allowedDomains.some(domain => html.includes(`${domain}/script.js`));
+ const hasWebsiteId = html.includes(`data-website-id="${websiteId}"`);
+
+ const isVerified = hasScriptSrc && hasWebsiteId;
+
+ if (isVerified) {
+ return {
+ success: isVerified,
+ id: websiteId
+ };
+ }
+
+ if (!hasScriptSrc) {
+ throw new ActionError({
+ code: "NOT_FOUND",
+ message: "Tracking script tag could not be found. Please ensure the script tag is added to your website's .",
+ });
+ }
+
+ if (!hasWebsiteId) {
+ throw new ActionError({
+ code: "BAD_REQUEST",
+ message: "Script detected, but the 'data-website-id' attribute is missing or incorrect for this website.",
+ });
+ }
+ },
+});
\ No newline at end of file
diff --git a/src/worker.ts b/src/worker.ts
index b4e9119..76f13d4 100644
--- a/src/worker.ts
+++ b/src/worker.ts
@@ -9,6 +9,6 @@ export function createExports(manifest: SSRManifest) {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return app.render(request, { locals: { runtime: { env, ctx } } });
},
- } satisfies ExportedHandler,
+ }
};
}
\ No newline at end of file