<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Suriya Prakash</title>
    <description>The latest articles on DEV Community by Suriya Prakash (@da_faq).</description>
    <link>https://dev.to/da_faq</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3426900%2F51ee7280-4f38-44fe-ad0d-e0889c7338f6.jpg</url>
      <title>DEV Community: Suriya Prakash</title>
      <link>https://dev.to/da_faq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/da_faq"/>
    <language>en</language>
    <item>
      <title>Real-Time Notifications in MERN using Socket.IO</title>
      <dc:creator>Suriya Prakash</dc:creator>
      <pubDate>Fri, 30 Jan 2026 09:00:57 +0000</pubDate>
      <link>https://dev.to/da_faq/real-time-notifications-in-mern-using-socketio-oe7</link>
      <guid>https://dev.to/da_faq/real-time-notifications-in-mern-using-socketio-oe7</guid>
      <description>&lt;p&gt;Real-time notifications are a must for modern applications—whether it’s admin alerts, user signups, or background job updates.&lt;/p&gt;

&lt;p&gt;In this article, I’ll walk through how to implement Socket.IO notifications in a MERN stack, using a real production-style backend setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧱 Tech Stack&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;li&gt;Express.js&lt;/li&gt;
&lt;li&gt;React.js&lt;/li&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Socket.IO&lt;/li&gt;
&lt;li&gt;node-cron (for scheduled background jobs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;📌 Use Case&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notify admins when a new user signs up&lt;/li&gt;
&lt;li&gt;Send real-time alerts when a technician’s work exceeds 2 hours&lt;/li&gt;
&lt;li&gt;Persist notifications in MongoDB&lt;/li&gt;
&lt;li&gt;Push notifications instantly to connected clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;⚙️ Server Setup (Socket.IO + Express)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, we create an HTTP server and attach Socket.IO to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const http = require("http");
const { Server } = require("socket.io");

const server = http.createServer(app);

const io = new Server(server, {
  cors: {
    origin: "*",
  },
});

app.set("io", io);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why app.set('io', io)?&lt;/strong&gt;&lt;br&gt;
This allows us to access the Socket.IO instance &lt;strong&gt;anywhere in our app&lt;/strong&gt;, including controllers and services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧩 Express Middleware &amp;amp; Routes&lt;/strong&gt;&lt;br&gt;
All normal Express middleware still works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.use(express.json());
app.use(cors());
app.use(helmet());
app.use("/api/admin", adminRouter);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Socket.IO runs alongside your REST APIs—not instead of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔔 Emitting Notifications on User Signup&lt;/strong&gt;&lt;br&gt;
When a user completes signup, we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save the notification in MongoDB&lt;/li&gt;
&lt;li&gt;Emit a real-time event via Socket.IO&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Signup Controller (Important Part)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const io = req.app.get('io');

const notification = await Notification.create({
  type: 'signup',
  message: `New user registered: ${user.basicInfo.fullName}`,
  userId: user._id,
  time: new Date(),
  read: false
});

io.emit('notification', notification);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What’s happening here?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;io.emit() broadcasts to all connected clients&lt;/li&gt;
&lt;li&gt;You can later replace this with:

&lt;ul&gt;
&lt;li&gt;io.to(userId).emit() (user-specific)&lt;/li&gt;
&lt;li&gt;io.to("admin").emit() (role-based rooms)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;⏱ Background Notifications using Cron Jobs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We also send notifications automatically when a technician works longer than 2 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cron Job Example&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cron.schedule('*/5 * * * * *', async () =&amp;gt; {
  const inProgressTechServices = await TechnicianUserService.find({
    status: "in-progress",
    adminNotified: { $ne: true }
  });

  for (const techService of inProgressTechServices) {
    let totalSeconds = techService.workDuration || 0;

    if (techService.workStartedAt) {
      totalSeconds += Math.floor((Date.now() - techService.workStartedAt) / 1000);
    }

    if (totalSeconds &amp;gt;= 7200) {
      await Notification.create({
        title: "Technician work exceeded 2 hours",
        message: "Service request exceeded time limit",
        type: "work_overdue"
      });

      techService.adminNotified = true;
      await techService.save();
    }
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notifications are automatic&lt;/li&gt;
&lt;li&gt;No duplicate alerts&lt;/li&gt;
&lt;li&gt;Works even if no user is logged in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;⚛️ Frontend (React) – Listening to Notifications&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the React side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { io } from "socket.io-client";

const socket = io("http://localhost:5000");

useEffect(() =&amp;gt; {
  socket.on("notification", (data) =&amp;gt; {
    console.log("New Notification:", data);
  });

  return () =&amp;gt; socket.off("notification");
}, []);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧠 Best Practices&lt;/p&gt;

&lt;p&gt;✅ Store notifications in DB&lt;br&gt;
✅ Emit only IDs or small payloads&lt;br&gt;
✅ Use Socket.IO rooms for scalability&lt;br&gt;
✅ Combine REST APIs + sockets&lt;br&gt;
❌ Don’t rely on sockets alone for persistence&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🎯 Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Socket.IO works best when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;REST handles data&lt;/li&gt;
&lt;li&gt;Sockets handle events&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>socketio</category>
      <category>node</category>
      <category>notifications</category>
      <category>mern</category>
    </item>
    <item>
      <title>Custom Bezier Tool in Konva.js with React + Redux</title>
      <dc:creator>Suriya Prakash</dc:creator>
      <pubDate>Thu, 28 Aug 2025 05:46:06 +0000</pubDate>
      <link>https://dev.to/da_faq/custom-bezier-tool-in-konvajs-with-react-redux-43p5</link>
      <guid>https://dev.to/da_faq/custom-bezier-tool-in-konvajs-with-react-redux-43p5</guid>
      <description>&lt;h2&gt;
  
  
  Building a Custom Bezier Tool in Konva.js (React + Redux)
&lt;/h2&gt;

&lt;p&gt;If you’ve ever tried building a &lt;strong&gt;Pen Tool&lt;/strong&gt; (like in Photoshop, Illustrator, or Figma), you’ll know how challenging it can be.&lt;br&gt;&lt;br&gt;
Doing this inside &lt;strong&gt;Konva.js&lt;/strong&gt; is &lt;strong&gt;not straightforward&lt;/strong&gt; — handling Bezier curves, multiple path options, and state management requires careful design.&lt;/p&gt;

&lt;p&gt;That’s why I’m sharing my implementation here — &lt;strong&gt;not as a polished final library&lt;/strong&gt;, but as &lt;strong&gt;knowledge-sharing&lt;/strong&gt; for anyone trying to implement a Pen/Bezier tool in a Konva.js + React + Redux stack.  &lt;/p&gt;

&lt;p&gt;Hopefully, this helps you avoid some of the pain points I went through.   &lt;/p&gt;
&lt;h2&gt;
  
  
  ⚡ Setup
&lt;/h2&gt;

&lt;p&gt;Install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install konva react-konva react-redux @reduxjs/toolkit react-icons

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🏗️ Redux Slice (toolSlice.js)
&lt;/h2&gt;

&lt;p&gt;Here’s how I structured my toolSlice to support Bezier options and control points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// toolSlice.js

// ...Bezier option state...
initialState: {
  // ...
  bezierOption: "Straight Segments",
  spiroPoints: [],
  bsplinePoints: [],
  controlPoints: [],
  // ...
},

reducers: {
  setBezierOption: (state, action) =&amp;gt; {
    state.bezierOption = action.payload;
  },
  addSpiroPoint: (state, action) =&amp;gt; {
    state.spiroPoints.push(action.payload);
  },
  addBSplinePoint: (state, action) =&amp;gt; {
    const point = action.payload;
    if (point &amp;amp;&amp;amp; typeof point.x === "number" &amp;amp;&amp;amp; typeof point.y === "number") {
      state.bsplinePoints.push(point);
    } else {
      console.error("Invalid point format in reducer:", point);
    }
  },
  clearBSplinePoints: (state) =&amp;gt; {
    state.bsplinePoints = [];
  },
  addControlPoint: (state, action) =&amp;gt; {
    state.controlPoints.push(action.payload);
    console.log("Updated controlPoints in Redux:", [...state.controlPoints]);
  },
  setControlPoints: (state, action) =&amp;gt; {
    console.log("Setting control points in Redux:", action.payload);
    state.controlPoints = action.payload;
  },
  clearControlPoints: (state) =&amp;gt; {
    state.controlPoints = [];
  },
  // ...other bezier-related reducers...
  addBezierPoint: (state, action) =&amp;gt; {
    const selectedLayer = state.layers[state.selectedLayerIndex];
    const lastBezierShape = selectedLayer.shapes.find(
      (shape) =&amp;gt; shape.type === "Bezier"
    );
    if (lastBezierShape) {
      lastBezierShape.points.push(...action.payload);
      console.log("Updated Bezier points in Redux:", lastBezierShape.points);
    } else {
      selectedLayer.shapes.push({
        id: `bezier-${Date.now()}`,
        type: "Bezier",
        points: action.payload,
        strokeColor: state.strokeColor,
        strokeWidth: 2,
      });
      console.log("New Bezier shape added:", action.payload);
    }
  },
  clearBezier: (state) =&amp;gt; {
    state.shapes = state.shapes.filter((shape) =&amp;gt; shape.type !== "Bezier");
  },
}

export const {
  setBezierOption,
  addSpiroPoint,
  addBSplinePoint,
  clearBSplinePoints,
  addControlPoint,
  setControlPoints,
  clearControlPoints,
  addBezierPoint,
  clearBezier,
} = toolSlice.actions;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎨 Panel Component (Panel.jsx)
&lt;/h2&gt;

&lt;p&gt;This is where I render the Bezier tool inside the Konva canvas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// panel.jsx

// ...Bezier tool cursor icon...
const toolCursors = {
  Bezier: &amp;lt;BsVectorPen size={20} color="black" /&amp;gt;,
};

// ...Bezier option state...
const bezierOption = useSelector((state) =&amp;gt; state.tool.bezierOption);

// ...Bezier drawing state...
const controlPoints = useSelector((state) =&amp;gt; state.tool.controlPoints);

// ...Bezier path rendering...
{
  selectedTool === "Bezier" &amp;amp;&amp;amp; (
    &amp;lt;Path
      data={getBezierPath()}
      stroke="black"
      strokeWidth={2}
      fill={isShapeClosed ? "rgba(0,0,0,0.1)" : "transparent"}
      closed={isShapeClosed}
    /&amp;gt;
  )
}

// ...Bezier path generator...
const getBezierPath = () =&amp;gt; {
    if (controlPoints.length &amp;lt; 2) {
      console.warn("Not enough control points to generate a path.");
      return "";
    }

    const [start, ...rest] = controlPoints;
    let path = `M ${start.x},${start.y}`;

    for (let i = 0; i &amp;lt; rest.length; i++) {
      const point = rest[i];
      path += ` L ${point.x},${point.y}`;
    }

    if (isShapeClosed) {
      path += ` Z`;
    }

    return path;
  };

// ...Bezier path from points...
const getBezierPathFromPoints = (points, isClosed) =&amp;gt; {
    if (points.length &amp;lt; 1) return "";

    const [start, ...rest] = points;
    let path = `M ${start.x},${start.y}`;

    for (let i = 0; i &amp;lt; rest.length; i++) {
      const point = rest[i];
      path += ` L ${point.x},${point.y}`;
    }

    if (isClosed) {
      path += ` Z`;
    }
    return path;
  };

// ...Bezier option select handler...
const handleOptionSelect = (option) =&amp;gt; {
  dispatch(setBezierOption(option));
  dispatch(clearSpiroPoints());
  dispatch(clearBSplinePoints());
  dispatch(clearParaxialPoints());
  dispatch(clearStraightPoints());
};

// ...Bezier path rendering for options...
{selectedTool === "Bezier" &amp;amp;&amp;amp; (
  bezierOption === "Straight Segments" ? (
    straightPoints.length &amp;gt; 1 &amp;amp;&amp;amp; (
      &amp;lt;Path
        data={`M ${straightPoints.map((p) =&amp;gt; `${p.x},${p.y}`).join(" L ")}${isShapeClosed ? " Z" : ""}`}
        stroke="black"
        strokeWidth={2}
        fill={isShapeClosed ? fillColor : "transparent"}
        closed={isShapeClosed}
      /&amp;gt;
    )
  ) : bezierOption === "Spiro Path" ? (
    renderSpiroPath()
  ) : bezierOption === "BSpline Path" ? (
    renderBSplinePath()
  ) : bezierOption === "Paraxial Line Segments" ? (
    renderParaxialSegments()
  ) : (
    controlPoints.length &amp;gt; 1 &amp;amp;&amp;amp; (
      &amp;lt;Path
        data={getBezierPath()}
        stroke="black"
        strokeWidth={2}
        fill={isShapeClosed ? fillColor : "transparent"}
        closed={isShapeClosed}
      /&amp;gt;
    )
  )
)}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🖱️ Interaction Handlers
&lt;/h2&gt;

&lt;p&gt;Dragging and closing paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ...Bezier points update...
const handleDragMove = (e, index) =&amp;gt; {
  if (selectedTool === "Bezier" &amp;amp;&amp;amp; bezierOption === "Spiro Path") {
      const { x, y } = e.target.position();
      dispatch(updateControlPoint({ index, point: { x, y } }));
    }
    if (selectedTool === "Bezier" &amp;amp;&amp;amp; bezierOption === "BSpline Path") {
      const { x, y } = e.target.position();
      dispatch(updateControlPoint({ index, point: { x, y } }));
    }
};

// ...Bezier double click handler...
const handleDoubleClick = () =&amp;gt; {
    if (newShape &amp;amp;&amp;amp; newShape.type === "Bezier") {
      dispatch(addShape(newShape));
      setNewShape(null);
      setIsDrawing(false);
    }
    if (selectedTool === "Bezier" &amp;amp;&amp;amp; bezierOption === "Spiro Path" &amp;amp;&amp;amp; spiroPoints.length &amp;gt; 1) {
      const pathData = generateSpiroPath(spiroPoints, 0.5, isShapeClosed);


      dispatch(
        addShape({
          id: `spiro-${Date.now()}`,
          type: "Spiro Path",
          path: pathData,
          stroke: strokeColor,
          strokeWidth: 2,
          fill: fillColor || "black",
        })
      );

      dispatch(clearSpiroPoints());
      return;
    }

    if (selectedTool === "Bezier" &amp;amp;&amp;amp; bezierOption === "BSpline Path" &amp;amp;&amp;amp; bsplinePoints.length &amp;gt; 1) {
      const pathData = generateBSplinePath(bsplinePoints, 0.5, true);

      dispatch(
        addShape({
          id: `bspline-${Date.now()}`,
          type: "Path",
          path: pathData,
          stroke: strokeColor,
          strokeWidth: 2,
          fill: isShapeClosed ? fillColor : "black",
        })
      );


      dispatch(clearBSplinePoints());
      setIsDrawing(false);
    }

    if (selectedTool === "Bezier" &amp;amp;&amp;amp; bezierOption === "Paraxial Line Segments") {
      if (paraxialPoints.length &amp;lt; 2) {
        console.warn("Not enough points to create a shape.");
        return;
      }


      let pathData = paraxialPoints
        .map((point, index) =&amp;gt; {
          return index === 0 ? `M ${point.x} ${point.y}` : `L ${point.x} ${point.y}`;
        })
        .join(" ");


      if (isShapeClosed) {
        pathData += " Z";
      }


      dispatch(
        addShape({
          id: `paraxial-${Date.now()}`,
          type: "Path",
          path: pathData,
          stroke: strokeColor,
          strokeWidth: 2,
          fill: isShapeClosed ? fillColor : "black",
        })
      );


      dispatch(clearParaxialPoints());
      setIsDrawing(false);
    }

    if (selectedTool === "Bezier" &amp;amp;&amp;amp; bezierOption === "Straight Segments" &amp;amp;&amp;amp; straightPoints.length &amp;gt; 1) {
      let pathData = straightPoints
        .map((point, index) =&amp;gt; (index === 0 ? `M ${point.x} ${point.y}` : `L ${point.x} ${point.y}`))
        .join(" ");
      if (isShapeClosed) pathData += " Z";
      dispatch(
        addShape({
          id: `straight-${Date.now()}`,
          type: "Path",
          path: pathData,
          stroke: strokeColor,
          strokeWidth: 2,
          fill: isShapeClosed ? fillColor : "transparent",
        })
      );
      dispatch(clearStraightPoints());
      setIsDrawing(false);
      setIsShapeClosed(false);
      return;
    }
  };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ## 🖼️ Rendering and Editing Bezier Shapes
&lt;/h2&gt;

&lt;p&gt;Finally, when rendering shapes from the Redux store, I handle Bezier shapes separately so they can be &lt;strong&gt;selected, dragged, and transformed&lt;/strong&gt; just like other shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (shape.type === "Bezier") {
  const isSelected = selectedShapeIds.includes(shape.id);
  return (
    &amp;lt;React.Fragment key={shape.id}&amp;gt;
      &amp;lt;Path
        ref={(node) =&amp;gt; {
          if (node) shapeRefs.current[shape.id] = node;
          else delete shapeRefs.current[shape.id];
          console.log("Bezier ref for", shape.id, node);
        }}
        id={shape.id}
        data={getBezierPathFromPoints(shape.points, shape.closed)}
        stroke={isSelected ? "blue" : shape.stroke || shape.strokeColor || "black"}
        strokeWidth={shape.strokeWidth || 2}
        fill={shape.fill || (shape.closed ? shape.fillColor : "transparent")}
        closed={shape.closed}
        rotation={shape.rotation || 0}
        draggable={
          !shape.locked &amp;amp;&amp;amp;
          selectedTool !== "Node" &amp;amp;&amp;amp;
          selectedTool !== "Mesh" &amp;amp;&amp;amp;
          selectedTool !== "Connector"
        }
        onDragMove={handleDragMove}
        dash={getDashArray(shape.strokeStyle)}
        onClick={(e) =&amp;gt; {
          e.cancelBubble = true;
          if (shape.locked) return;
          if (!selectedShapeIds.includes(shape.id)) {
            dispatch(selectShape(shape.id));
          }
        }}
        onMouseDown={(e) =&amp;gt; {
          e.cancelBubble = true;
        }}
        onTransformStart={(e) =&amp;gt; {
          e.cancelBubble = true;
        }}
        onTransformEnd={(e) =&amp;gt; handleBezierTransformEnd(e, shape)}
        onDragEnd={(e) =&amp;gt; {
          const { x, y } = e.target.position();
          dispatch(updateShapePosition({ id: shape.id, x, y }));
        }}
      /&amp;gt;
      {isSelected &amp;amp;&amp;amp;
        !shape.locked &amp;amp;&amp;amp;
        shapeRefs.current[shape.id] &amp;amp;&amp;amp;
        selectedTool !== "Node" &amp;amp;&amp;amp; (
          &amp;lt;Transformer
            ref={transformerRef}
            nodes={selectedShapeIds
              .map((id) =&amp;gt; shapeRefs.current[id])
              .filter(Boolean)}
            boundBoxFunc={(oldBox, newBox) =&amp;gt; {
              if (newBox.width &amp;lt; 5 || newBox.height &amp;lt; 5) {
                return oldBox;
              }
              return newBox;
            }}
            enabledAnchors={[
              "top-left",
              "top-center",
              "top-right",
              "middle-left",
              "middle-right",
              "bottom-left",
              "bottom-center",
              "bottom-right",
            ]}
            skewEnabled={true}
          /&amp;gt;
        )}
    &amp;lt;/React.Fragment&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Flow Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[User Input]
   |
   |-- Click / Drag / Double-Click
   v
[Panel Component]
   |
   |-- selectedTool === "Bezier" ?
         |
         +--&amp;gt; Yes → Read bezierOption &amp;amp; controlPoints from Redux
         |          |
         |          +--&amp;gt; Call appropriate path generator:
         |                 |
         |                 +--&amp;gt; Straight Segments → getStraightPath()
         |                 |
         |                 +--&amp;gt; Spiro Path       → generateSpiroPath()
         |                 |
         |                 +--&amp;gt; BSpline Path     → generateBSplinePath()
         |                 |
         |                 +--&amp;gt; Paraxial Segments → renderParaxialSegments()
         |                 |
         |                 +--&amp;gt; Default / Custom → getBezierPath()
         |
         +--&amp;gt; No → Ignore / other tools
   v
[Interaction Handlers]
   |
   |-- handleDragMove → update controlPoints in Redux
   |-- handleDoubleClick → generate pathData
          |
          |-- Dispatch addShape / clearPoints actions
   v
[Redux Slice (toolSlice)]
   |
   |-- Stores:
   |     - controlPoints
   |     - straightPoints / spiroPoints / bsplinePoints / paraxialPoints
   |     - bezierOption
   |-- Reducers update state based on dispatched actions
   v
[Redux Store Updates]
   |
   |-- Triggers re-render in Panel
   v
[Bezier Rendering]
   |
   |-- Uses &amp;lt;Path /&amp;gt; to draw shape
   |-- stroke / fill / closed / draggable
   |-- Calls getBezierPathFromPoints if rendering saved shapes
   v
[Optional Transformer / Editing]
   |
   |-- Shows transformer if shape selected
   |-- Drag / Scale / Rotate → dispatch updateShapePosition
   v
[Canvas Updated / Shape Rendered]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ✅ Conclusion
&lt;/h2&gt;

&lt;p&gt;This setup lets me switch between Bezier variants and manage them cleanly in Redux.&lt;br&gt;
It’s flexible enough to expand with more path types and keeps drawing logic decoupled from state.&lt;/p&gt;

</description>
      <category>konvajs</category>
      <category>beziertool</category>
      <category>pentool</category>
      <category>react</category>
    </item>
    <item>
      <title>Centralized MTurk HIT Catcher with PHP &amp; Userscripts</title>
      <dc:creator>Suriya Prakash</dc:creator>
      <pubDate>Wed, 27 Aug 2025 07:13:10 +0000</pubDate>
      <link>https://dev.to/da_faq/centralized-mturk-hit-catcher-with-php-userscripts-1a0a</link>
      <guid>https://dev.to/da_faq/centralized-mturk-hit-catcher-with-php-userscripts-1a0a</guid>
      <description>&lt;p&gt;Managing multiple MTurk accounts can be tedious. What if you could paste multiple HIT set IDs once and let a userscript automatically accept HITs for all connected accounts?&lt;/p&gt;

&lt;p&gt;Here’s how I built a centralized MTurk HIT catcher using PHP, a JSON endpoint, and a userscript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Central PHP page to save and manage HIT set IDs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Toggle catching HITs on/off.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;JSON endpoint for userscripts to fetch IDs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Userscript auto-accepts HITs for multiple accounts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Server rendering using Ngrok for remote access.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  PHP Controller (hitaccept.php)
&lt;/h2&gt;

&lt;p&gt;This page allows you to save HIT set IDs, see the current status, and serve IDs as JSON.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php
$file = __DIR__ . "/hit_ids.json";
$statusFile = __DIR__ . "/status.txt";

if (isset($_GET["toggle"])) {
  $status = ($_GET["toggle"] === "off") ? "off" : "on";
  file_put_contents($statusFile, $status);
  echo "Status set to $status. &amp;lt;a href='hitaccept.php'&amp;gt;Back&amp;lt;/a&amp;gt;";
  exit;
}

if ($_SERVER["REQUEST_METHOD"] === "POST") {
  $data = $_POST["hit_ids"] ?? "";
  $ids = array_filter(array_map("trim", explode("\n", $data)));
  file_put_contents($file, json_encode($ids));
  echo "Saved! &amp;lt;a href='hitaccept.php'&amp;gt;Back&amp;lt;/a&amp;gt;";
  exit;
}

if (isset($_GET["json"])) {
  header("Content-Type: application/json");
  $status = file_exists($statusFile) ? trim(file_get_contents($statusFile)) : "on";
  $ids = file_exists($file) ? file_get_contents($file) : "[]";
  echo json_encode([
    "status" =&amp;gt; $status,
    "ids" =&amp;gt; json_decode($ids, true)
  ]);
  exit;
}

$current = file_exists($file) ? file_get_contents($file) : "[]";
$status = file_exists($statusFile) ? trim(file_get_contents($statusFile)) : "on";
?&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;title&amp;gt;MTurk HIT Controller&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h2&amp;gt;📋 Paste HIT Set IDs (one per line)&amp;lt;/h2&amp;gt;
  &amp;lt;form method="POST"&amp;gt;
    &amp;lt;textarea name="hit_ids" rows="6" cols="60"&amp;gt;&amp;lt;/textarea&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;
    &amp;lt;button type="submit"&amp;gt;💾 Save IDs&amp;lt;/button&amp;gt;
  &amp;lt;/form&amp;gt;

  &amp;lt;h3&amp;gt;Current HIT IDs&amp;lt;/h3&amp;gt;
  &amp;lt;pre&amp;gt;&amp;lt;?php echo htmlspecialchars($current); ?&amp;gt;&amp;lt;/pre&amp;gt;

  &amp;lt;h3&amp;gt;Status: &amp;lt;?php echo strtoupper($status); ?&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;p&amp;gt;
    &amp;lt;a href="?toggle=on"&amp;gt;▶️ Start Catching&amp;lt;/a&amp;gt; |
    &amp;lt;a href="?toggle=off"&amp;gt;⏹ Stop Catching&amp;lt;/a&amp;gt;
  &amp;lt;/p&amp;gt;

  &amp;lt;p&amp;gt;JSON endpoint: &amp;lt;a href="hitaccept.php?json"&amp;gt;hitaccept.php?json&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Save multiple HIT set IDs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Toggle catching on/off.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Serve a JSON endpoint for your userscript.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Userscript for Auto-Accepting HITs
&lt;/h2&gt;

&lt;p&gt;Install in &lt;strong&gt;Tampermonkey&lt;/strong&gt; or &lt;strong&gt;Greasemonkey&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ==UserScript==
// @name         MTurk Central HIT Catcher
// @namespace    yourdomain.com
// @version      1.0
// @description  Accept HITs automatically from set_ids provided by central server
// @match        https://worker.mturk.com/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    const SERVER_URL = "https://your-ngrok-url.ngrok-free.app/hitaccept.php?json";
    const FETCH_INTERVAL = 10000; // 10s

    function fetchSetIdsAndCatch() {
        GM_xmlhttpRequest({
            method: "GET",
            url: SERVER_URL,
            onload: function(response) {
                try {
                    let data = JSON.parse(response.responseText);
                    if (data.status === "off") {
                        console.log("⏹ Catcher stopped by server.");
                        return;
                    }

                    let ids = data.ids || [];
                    ids.forEach(id =&amp;gt; {
                        let acceptUrl = `https://worker.mturk.com/projects/${id}/tasks/accept_random?format=json`;
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: acceptUrl,
                            onload: function(res) {
                                if (res.status === 200 &amp;amp;&amp;amp; res.responseText.includes("task_id")) {
                                    console.log("✅ Accepted HIT:", id);
                                } else {
                                    console.log("❌ No HITs available for:", id);
                                }
                            }
                        });
                    });

                } catch (e) {
                    console.error("Error parsing JSON:", e);
                }
            }
        });
    }

    setInterval(fetchSetIdsAndCatch, FETCH_INTERVAL);
    console.log("🚀 MTurk Central HIT Catcher started, polling every", FETCH_INTERVAL/1000, "seconds");
})();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running the Project
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start your PHP server locally:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php -S localhost:8000

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Expose it to the internet via Ngrok:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngrok http 8000

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Replace SERVER_URL in your userscript with your Ngrok URL:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-ngrok-url.ngrok-free.app/hitaccept.php?json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open the &lt;strong&gt;Tampermonkey&lt;/strong&gt; dashboard and activate the script.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Go to hitaccept.php, paste your HIT set IDs, and click &lt;strong&gt;Start Catching&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check your browser console for logs:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;HIT accepted&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No HITs available&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;With this setup, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Centralize multiple HIT IDs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Control the catching process via a web interface.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automatically accept HITs across multiple accounts.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s perfect for heavy MTurk users or small teams who need &lt;strong&gt;automation without manual repetition&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxoan96xdld1l9y8vn2r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxoan96xdld1l9y8vn2r.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mturk</category>
      <category>php</category>
      <category>userscripts</category>
      <category>automation</category>
    </item>
    <item>
      <title>Automating MTurk HIT Acceptance with a Tampermonkey Userscript</title>
      <dc:creator>Suriya Prakash</dc:creator>
      <pubDate>Wed, 13 Aug 2025 07:06:52 +0000</pubDate>
      <link>https://dev.to/da_faq/automating-mturk-hit-acceptance-with-a-tampermonkey-userscript-48on</link>
      <guid>https://dev.to/da_faq/automating-mturk-hit-acceptance-with-a-tampermonkey-userscript-48on</guid>
      <description>&lt;p&gt;If you’ve ever worked on Amazon Mechanical Turk (MTurk), you know how time-sensitive high-paying HITs can be. Blink, and they’re gone. Manually scanning for HITs that meet your pay criteria can be tedious — and sometimes impossible when competing with other workers.&lt;/p&gt;

&lt;p&gt;That’s why I built a &lt;strong&gt;Tampermonkey userscript&lt;/strong&gt; to automatically accept MTurk HITs based on two conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reward threshold&lt;/strong&gt; (minimum pay I’m willing to work for)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set ID&lt;/strong&gt; (to target specific requesters or tasks)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post walks you through &lt;strong&gt;how I implemented it&lt;/strong&gt;, so you can understand the logic, customize it for your own needs, and maybe even improve it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Tampermonkey?
&lt;/h2&gt;

&lt;p&gt;Tampermonkey is a browser extension that allows you to run custom JavaScript on specified web pages. For MTurk, this means you can inject your own automation into the interface without modifying any server-side code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Easy to install&lt;/li&gt;
&lt;li&gt;Works across browsers&lt;/li&gt;
&lt;li&gt;Flexible and scriptable&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Manually hunting for high-paying HITs wastes time and causes missed opportunities. I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic filtering&lt;/strong&gt; so only HITs above a certain reward appear in my workflow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Direct acceptance&lt;/strong&gt; of those HITs without extra clicks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My Approach
&lt;/h2&gt;

&lt;p&gt;I broke the problem into three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fetch &amp;amp; parse HIT data&lt;/strong&gt; from the MTurk search results page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apply filters&lt;/strong&gt; based on reward and set ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trigger acceptance&lt;/strong&gt; via MTurk’s accept URL when criteria are met.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install Tampermonkey&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Chrome: &lt;code&gt;https://tampermonkey.net/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After installation, click the Tampermonkey icon → "Create a new script."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Script&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Script Metadata&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tampermonkey scripts start with a header that describes the script, permissions, and the pages it should run on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ==UserScript==
// @name         MTurk Auto-HIT Catcher
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Automatically accepts MTurk HITs based on reward &amp;amp; HIT Set ID
// @author       YourName
// @match        https://worker.mturk.com/match
// @grant        GM.getValue
// @grant        GM.setValue
// ==/UserScript==

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Setup &amp;amp; Global Variables&lt;/strong&gt;&lt;br&gt;
We use an async function so we can load stored data (like the block list) before starting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(async function() {
    'use strict';

    var blockList = await loadBlockList();
    var baseUrl = 'https://worker.mturk.com';
    var tasks = {};

    var isTimerRunning = false;
    var timer;

    let isSearching = false;
    let acceptedCount = 0;
    const maxAcceptedHits = 10;
    let searchHitId = null;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Adding the Control Panel to MTurk&lt;/strong&gt;&lt;br&gt;
We create an HTML template for our settings and inject it into MTurk’s DOM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function appendHTMLElements() {
    var rewardTemplate = `
&amp;lt;div class="row"&amp;gt;
    &amp;lt;div class="col-sm-3 form-inline"&amp;gt;
        &amp;lt;h3&amp;gt;&amp;lt;strong&amp;gt;Parameters&amp;lt;/strong&amp;gt;&amp;lt;/h3&amp;gt;
        &amp;lt;label for="filters-min-reward"&amp;gt;
            &amp;lt;span class="m-r-xs"&amp;gt;Pays at least $&amp;lt;/span&amp;gt;
            &amp;lt;input class="form-control reward-filter" id="filters-min-reward" value="1" type="number" step="0.01"&amp;gt;
        &amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;!-- Additional UI controls omitted for brevity --&amp;gt;
&amp;lt;/div&amp;gt;
    `;

    var el = document.querySelector('#MainContent');
    if (el) {
        el.innerHTML = rewardTemplate;
    }

    // Attach button handlers
    document.getElementById("timer").onclick = toggleTimer;
    document.getElementById("block").onclick = blockRequestor;
    document.getElementById("search-toggle").onclick = toggleSearchByHitId;
    document.getElementById("close-captcha").onclick = () =&amp;gt; {
        document.getElementById("captcha-modal").style.display = "none";
    };

    updateBlockEl();
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Managing Blocked Requesters&lt;/strong&gt;&lt;br&gt;
We can save unwanted requesters so they’re ignored in future searches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function blockRequestor() {
    var block = getBlockText();
    if (blockList.indexOf(block) &amp;lt; 0) {
        blockList.push(block);
        await saveBlockList();
    }
}

function updateBlockEl() {
    var listEl = document.getElementById('block-list');
    listEl.innerHTML = '';
    for (var id of blockList) {
        var blockEl = document.createElement('span');
        blockEl.id = id;
        blockEl.innerHTML = `&amp;lt;span&amp;gt;&amp;lt;a href="#"&amp;gt;&amp;lt;i class="fa fa-minus-circle"&amp;gt;&amp;lt;/i&amp;gt;${id}&amp;lt;/a&amp;gt;&amp;lt;/span&amp;gt;`;
        blockEl.onclick = removeBlockList;
        listEl.appendChild(blockEl);
    }
}

async function removeBlockList() {
    blockList.splice(blockList.indexOf(this.id), 1);
    await saveBlockList();
}

async function loadBlockList() {
    return JSON.parse(await GM.getValue("autoHitBL", '[]'));
}

async function saveBlockList() {
    await GM.setValue("autoHitBL", JSON.stringify(blockList));
    updateBlockEl();
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Searching &amp;amp; Auto-Accepting by HIT Set ID&lt;/strong&gt;&lt;br&gt;
This is the core automation — repeatedly trying to accept a HIT until it succeeds or reaches the limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function toggleSearchByHitId() {
    const btn = document.getElementById("search-toggle");

    if (!isSearching) {
        const hitId = document.getElementById("search-hit-id").value.trim();
        if (!hitId) {
            alert("Please enter a HIT Set ID.");
            return;
        }
        searchHitId = hitId;
        acceptedCount = 0;

        const acceptUrl = `/projects/${hitId}/tasks/accept_random?ref=w_pl_prvw`;
        const task = { hit_set_id: hitId, accept_project_task_url: acceptUrl };

        isSearching = true;
        btn.textContent = "Stop";
        btn.classList.replace("btn-primary", "btn-danger");

        acceptMultipleHits(task);
    } else {
        isSearching = false;
        searchHitId = null;
        btn.textContent = "Start";
        btn.classList.replace("btn-danger", "btn-primary");
    }
}

function acceptMultipleHits(task) {
    async function attempt() {
        if (!isSearching || acceptedCount &amp;gt;= maxAcceptedHits) return;

        try {
            const res = await fetch(baseUrl + task.accept_project_task_url, { credentials: 'include' });
            const html = await res.text();

            if (html.toLowerCase().includes("captcha")) {
                isSearching = false;
                document.getElementById("captcha-modal").style.display = "flex";
                return;
            }

            // Extract HIT details and add to accepted list
            addAcceptedHitToList(task);
            acceptedCount++;

        } catch (err) {
            console.error("Error accepting HIT:", err);
        }

        if (isSearching) setTimeout(attempt, 750);
    }
    attempt();
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Displaying Accepted HITs&lt;/strong&gt;&lt;br&gt;
We show a live-updating list of accepted tasks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function addAcceptedHitToList(task) {
    const listEl = document.getElementById("accepted-hits-list");
    const listItem = document.createElement("li");

    const fullUrl = task.work_url.startsWith('http') ? task.work_url : `${baseUrl}${task.work_url}`;
    listItem.innerHTML = `
        &amp;lt;div&amp;gt;
            &amp;lt;strong&amp;gt;${task.requester_name}&amp;lt;/strong&amp;gt; —
            &amp;lt;a href="${task.accept_project_task_url}" target="_blank"&amp;gt;${task.title}&amp;lt;/a&amp;gt;
            — &amp;lt;span style="color: green;"&amp;gt;$${task.monetary_reward.amount_in_dollars}&amp;lt;/span&amp;gt;
            — &amp;lt;a href="${fullUrl}" target="_blank"&amp;gt;[Work Link]&amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
    `;
    listEl.insertBefore(listItem, listEl.firstChild);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fetching &amp;amp; Processing New HITs&lt;/strong&gt;&lt;br&gt;
The script can also scan for new tasks automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getNewTasks() {
    if (isSearching) return;
    getContent('/?page_size=20&amp;amp;filters[qualified]=true&amp;amp;filters[masters]=false&amp;amp;sort=updated_desc&amp;amp;filters[min_reward]=' + getMinReward(),
        function(err, doc) {
            var currTasks = parseTasks(doc);
            for (let task of currTasks) {
                if (task.assignable_hits_count &amp;gt; 0 &amp;amp;&amp;amp; blockList.indexOf(task.requester_id) &amp;lt; 0) {
                    processTask(task);
                    break;
                }
            }
        });
}

async function processTask(task) {
    try {
        const res = await fetch(baseUrl + task.accept_project_task_url, { credentials: 'include' });
        task.work_url = res.url;
        task.isAccepted = true;
        updateTaskEl(task);
        addAcceptedHitToList(task);
    } catch (err) {
        console.error("Error processing task:", err);
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Utility Functions&lt;/strong&gt;&lt;br&gt;
Helpers for fetching and parsing HITs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getContent(url, callback) {
    var httpRequest = new XMLHttpRequest();
    httpRequest.open('GET', url);
    httpRequest.send();
    httpRequest.onreadystatechange = function() {
        if (httpRequest.readyState === XMLHttpRequest.DONE) {
            if (httpRequest.status === 200 || httpRequest.status === 403) {
                var doc = new DOMParser().parseFromString(httpRequest.responseText, "text/html");
                callback(null, doc);
            }
        }
    }
}

function parseTasks(doc) {
    let el = doc.querySelector('div[data-react-class*="HitSetTable"]');
    if (el) {
        return JSON.parse(el.getAttribute("data-react-props").replace('&amp;amp;quot;', '"')).bodyData;
    }
    return [];
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;This script is for &lt;strong&gt;educational purposes only&lt;/strong&gt;. Automating HIT acceptance may violate MTurk’s Terms of Service. Always check the rules and use responsibly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;With Tampermonkey, we can build a custom automation layer on MTurk that saves time, filters tasks, and streamlines work. You can extend this script with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;More filters (qualifications, keywords)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Notifications when HITs are accepted&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A UI for setting parameters without editing code&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tampermonkey</category>
      <category>mturk</category>
      <category>userscript</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
