p2p poll logic added
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
# P2P Poll App
|
# P2P Poll App
|
||||||
|
Remove-Item -Recurse -Force node_modules, .next
|
||||||
@@ -1,20 +1,12 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "p2p polling",
|
||||||
description: "Generated by create next app",
|
description: "creating a p2p-polling app",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -25,7 +17,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={` h-full antialiased`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
<body className="min-h-full flex flex-col">{children}</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
102
app/page.tsx
102
app/page.tsx
@@ -1,65 +1,47 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import usePeerManager from "../hooks/usePeerManager";
|
||||||
|
import usePollManager from "../hooks/usePollManager";
|
||||||
|
import PollCreation from "../components/PollCreation";
|
||||||
|
import PollActive from "../components/PollActive";
|
||||||
|
import PeersList from "../components/PeersList";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const peerManager = usePeerManager();
|
||||||
|
const pollManager = usePollManager(peerManager);
|
||||||
|
|
||||||
|
const [connectId, setConnectId] = useState("");
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div className="p-6 space-y-4">
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<h1 className="text-2xl font-bold">P2P Poll App</h1>
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
{/* Connect */}
|
||||||
src="/next.svg"
|
<div className="flex gap-2">
|
||||||
alt="Next.js logo"
|
<input
|
||||||
width={100}
|
className="border p-2"
|
||||||
height={20}
|
placeholder="Peer ID"
|
||||||
priority
|
value={connectId}
|
||||||
|
onChange={(e) => setConnectId(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
<button
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
className="bg-blue-500 text-white px-4"
|
||||||
To get started, edit the page.tsx file.
|
onClick={() => peerManager.connectToPeer(connectId)}
|
||||||
</h1>
|
>
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
Connect
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
</button>
|
||||||
<a
|
</div>
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
<p>Your ID: {peerManager.peerId}</p>
|
||||||
>
|
|
||||||
Templates
|
{pollManager.poll ? (
|
||||||
</a>{" "}
|
<PollActive pollManager={pollManager} peerId={peerManager.peerId} />
|
||||||
or the{" "}
|
) : (
|
||||||
<a
|
<PollCreation pollManager={pollManager} />
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
)}
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
<PeersList peers={peerManager.peers} />
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
0
components/Modal.tsx
Normal file
0
components/Modal.tsx
Normal file
22
components/Notification.tsx
Normal file
22
components/Notification.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
type NotificationType = "info" | "success" | "error";
|
||||||
|
|
||||||
|
interface NotificationProps {
|
||||||
|
message: string;
|
||||||
|
type?: NotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Notification({ message, type = "info" }: NotificationProps) {
|
||||||
|
const colors: Record<NotificationType, string> = {
|
||||||
|
info: "bg-blue-100 text-blue-700",
|
||||||
|
success: "bg-green-100 text-green-700",
|
||||||
|
error: "bg-red-100 text-red-700",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`px-4 py-2 rounded shadow ${colors[type]}`}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
components/PeersList.tsx
Normal file
14
components/PeersList.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
export default function PeersList({ peers }: { peers: string[] }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Peers</h3>
|
||||||
|
{peers.length === 0 ? (
|
||||||
|
<p>No peers</p>
|
||||||
|
) : (
|
||||||
|
peers.map((p) => <div key={p}>{p}</div>)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
components/PollActive.tsx
Normal file
22
components/PollActive.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import PollOption from "./PollOption";
|
||||||
|
|
||||||
|
export default function PollActive({ pollManager, peerId }: any) {
|
||||||
|
const poll = pollManager.poll;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold">{poll.question}</h2>
|
||||||
|
|
||||||
|
{poll.options.map((opt: any) => (
|
||||||
|
<PollOption
|
||||||
|
key={opt.id}
|
||||||
|
option={opt}
|
||||||
|
pollManager={pollManager}
|
||||||
|
peerId={peerId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
components/PollCreation.tsx
Normal file
40
components/PollCreation.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function PollCreation({ pollManager }: any) {
|
||||||
|
const [question, setQuestion] = useState("");
|
||||||
|
const [options, setOptions] = useState<string[]>(["", ""]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="border p-2 w-full"
|
||||||
|
placeholder="Question"
|
||||||
|
value={question}
|
||||||
|
onChange={(e) => setQuestion(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{options.map((opt, i) => (
|
||||||
|
<input
|
||||||
|
key={i}
|
||||||
|
className="border p-2 w-full mt-2"
|
||||||
|
placeholder={`Option ${i + 1}`}
|
||||||
|
value={opt}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newOpts = [...options];
|
||||||
|
newOpts[i] = e.target.value;
|
||||||
|
setOptions(newOpts);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="bg-green-500 text-white px-4 mt-2"
|
||||||
|
onClick={() => pollManager.createPoll(question, options)}
|
||||||
|
>
|
||||||
|
Create Poll
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
components/PollOption.tsx
Normal file
12
components/PollOption.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
export default function PollOption({ option, pollManager, peerId }: any) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="border p-2 mt-2 cursor-pointer"
|
||||||
|
onClick={() => pollManager.vote(option.id, peerId)}
|
||||||
|
>
|
||||||
|
{option.text} - {option.votes} votes
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
hooks/usePeerManager.ts
Normal file
58
hooks/usePeerManager.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import Peer from "peerjs";
|
||||||
|
|
||||||
|
export default function usePeerManager() {
|
||||||
|
const [peerId, setPeerId] = useState<string | null>(null);
|
||||||
|
const [peers, setPeers] = useState<string[]>([]);
|
||||||
|
const peerRef = useRef<Peer | null>(null);
|
||||||
|
const connectionsRef = useRef<Map<string, any>>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const peer = new Peer();
|
||||||
|
peerRef.current = peer;
|
||||||
|
|
||||||
|
peer.on("open", (id) => {
|
||||||
|
setPeerId(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
peer.on("connection", (conn) => {
|
||||||
|
conn.on("open", () => {
|
||||||
|
connectionsRef.current.set(conn.peer, conn);
|
||||||
|
setPeers(Array.from(connectionsRef.current.keys()));
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on("data", (data) => {
|
||||||
|
console.log("Received:", data);
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on("close", () => {
|
||||||
|
connectionsRef.current.delete(conn.peer);
|
||||||
|
setPeers(Array.from(connectionsRef.current.keys()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
peer.destroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const connectToPeer = (id: string) => {
|
||||||
|
if (!peerRef.current) return;
|
||||||
|
const conn = peerRef.current.connect(id);
|
||||||
|
|
||||||
|
conn.on("open", () => {
|
||||||
|
connectionsRef.current.set(conn.peer, conn);
|
||||||
|
setPeers(Array.from(connectionsRef.current.keys()));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const broadcast = (data: any) => {
|
||||||
|
connectionsRef.current.forEach((conn) => {
|
||||||
|
if (conn.open) conn.send(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { peerId, peers, connectToPeer, broadcast };
|
||||||
|
}
|
||||||
68
hooks/usePollManager.ts
Normal file
68
hooks/usePollManager.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
votes: number;
|
||||||
|
voters: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Poll = {
|
||||||
|
id: string;
|
||||||
|
question: string;
|
||||||
|
options: Option[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function usePollManager(peerManager: any) {
|
||||||
|
const [poll, setPoll] = useState<Poll | null>(null);
|
||||||
|
const [myVote, setMyVote] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const createPoll = (question: string, options: string[]) => {
|
||||||
|
const newPoll: Poll = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
question,
|
||||||
|
options: options.map((opt, i) => ({
|
||||||
|
id: `opt-${i}`,
|
||||||
|
text: opt,
|
||||||
|
votes: 0,
|
||||||
|
voters: [],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
setPoll(newPoll);
|
||||||
|
peerManager.broadcast({ type: "poll", poll: newPoll });
|
||||||
|
};
|
||||||
|
|
||||||
|
const vote = (optionId: string, peerId: string) => {
|
||||||
|
if (!poll) return;
|
||||||
|
|
||||||
|
const updated = { ...poll };
|
||||||
|
|
||||||
|
updated.options.forEach((opt) => {
|
||||||
|
const index = opt.voters.indexOf(peerId);
|
||||||
|
if (index !== -1) {
|
||||||
|
opt.voters.splice(index, 1);
|
||||||
|
opt.votes--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const option = updated.options.find((o) => o.id === optionId);
|
||||||
|
if (!option) return;
|
||||||
|
|
||||||
|
option.votes++;
|
||||||
|
option.voters.push(peerId);
|
||||||
|
|
||||||
|
setPoll(updated);
|
||||||
|
setMyVote(optionId);
|
||||||
|
|
||||||
|
peerManager.broadcast({
|
||||||
|
type: "vote",
|
||||||
|
optionId,
|
||||||
|
voterId: peerId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { poll, createPoll, vote, myVote };
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
experimental: {
|
||||||
|
turbopackFileSystemCacheForDev: true,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
6660
package-lock.json
generated
Normal file
6660
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.2.0",
|
"next": "16.2.0",
|
||||||
|
"peerjs": "^1.5.5",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "19.2.4"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user