Add Webapp
41
webapp/.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
45
webapp/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run dev -- --inspect"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side (Firefox)",
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000",
|
||||
"reAttach": true,
|
||||
"pathMappings": [
|
||||
{
|
||||
"url": "webpack://_N_E",
|
||||
"path": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/node_modules/next/dist/bin/next",
|
||||
"runtimeArgs": ["--inspect"],
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithEdge",
|
||||
"killOnServerStop": true,
|
||||
"pattern": "- Local:.+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
25
webapp/BUILD.ps1
Normal file
@ -0,0 +1,25 @@
|
||||
# echo "Building"
|
||||
## PSVersionTable.PSVersion
|
||||
## winget install --id Microsoft.Powershell --source winget
|
||||
|
||||
mkdir dist
|
||||
npm run build
|
||||
cp -r .next/static .next/standalone/CoffeeChat/portal/.next
|
||||
cp -r .next/standalone/CoffeeChat/portal dist
|
||||
Compress-Archive -Path dist -DestinationPath dist.zip -Force
|
||||
Remove-Item dist -Recurse -Force
|
||||
|
||||
# Create an SSH session
|
||||
#$remoteUser = "cinton"
|
||||
#$remoteHost = "rootbranch.co.za:922"
|
||||
#$session = New-PSSession -HostName "rootbranch.co.za:922" -UserName "cinton" -SSHTransport
|
||||
|
||||
# Copy file from Windows to Linux
|
||||
#Copy-Item -Path dist.zip -Destination "/home/clinton/dist.zip" -ToSession $session
|
||||
|
||||
# Copy file from Linux to Windows
|
||||
# Copy-Item -Path "/home/username/file.txt" -Destination "C:\path\to\local\file.txt" -FromSession $session
|
||||
|
||||
# Close the session
|
||||
#Remove-PSSession $session
|
||||
|
||||
42
webapp/README.md
Normal file
@ -0,0 +1,42 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
||||
|
||||
## Publish
|
||||
|
||||
npm config set @test:registry=https://gitea.example.com/api/packages/testuser/npm/
|
||||
npm config set -- '//gitea.example.com/api/packages/testuser/npm/:_authToken' "personal_access_token"
|
||||
35
webapp/actions/auth/AuthActions.ts
Normal file
@ -0,0 +1,35 @@
|
||||
"use server";
|
||||
|
||||
import { CallApi } from "@/helper/api/ApiConnector";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect, RedirectType } from "next/navigation";
|
||||
|
||||
export async function LoginFormAction(formData: FormData) {
|
||||
console.log(`LoginFormAction`, formData);
|
||||
const res = await fetch(`http://localhost:8081/api/auth/signin`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: formData.get("email") as string,
|
||||
password: formData.get("password") as string,
|
||||
}),
|
||||
}).catch((e) => console.error(e));
|
||||
// .then(async (res) => {
|
||||
const resp = await (res as Response).json();
|
||||
// console.log(resp)
|
||||
if (!resp.error) {
|
||||
const cj = await cookies();
|
||||
cj.set("ccsession", resp.result);
|
||||
redirect(`/dashboard`, RedirectType.replace);
|
||||
}
|
||||
}
|
||||
|
||||
export async function RegisterFormAction(formData: FormData) {
|
||||
await CallApi(`/auth/signup`, "POST", {
|
||||
name: "",
|
||||
email: formData.get("email") as string,
|
||||
password: formData.get("password") as string,
|
||||
});
|
||||
}
|
||||
7
webapp/actions/room/RoomActions.ts
Normal file
@ -0,0 +1,7 @@
|
||||
'use server'
|
||||
|
||||
import { CallApi } from "@/helper/api/ApiConnector"
|
||||
|
||||
export async function getRooms(){
|
||||
return await CallApi("/rooms","GET")
|
||||
}
|
||||
8
webapp/actions/users/UserActions.ts
Normal file
@ -0,0 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { CallApi } from "@/helper/api/ApiConnector";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function ListUsers() {
|
||||
return await CallApi(`/users`,"GET").catch(e => console.error(e))
|
||||
}
|
||||
19
webapp/app/call/page.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function CallScreen() {
|
||||
return (
|
||||
<div className='w-full flex flex-col h-screen'>
|
||||
<div className='flex justify-evenly'>
|
||||
<button>Call</button>
|
||||
<button>White board</button>
|
||||
<button>Settings</button>
|
||||
</div>
|
||||
<div className='grow bg-blue-200 flex'>
|
||||
<div className='grow'>
|
||||
<video />
|
||||
</div>
|
||||
<div>Chat</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
webapp/app/dashboard/page.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { ListUsers } from '@/actions/users/UserActions'
|
||||
import ChatBox from '@/components/chat/ChatBox'
|
||||
import React from 'react'
|
||||
|
||||
export default async function LoggedIn() {
|
||||
const users = await ListUsers()
|
||||
return (<>
|
||||
<div>{JSON.stringify(users)}</div>
|
||||
<ChatBox />
|
||||
</>)
|
||||
}
|
||||
BIN
webapp/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 279 KiB |
138
webapp/app/globals.css
Normal file
@ -0,0 +1,138 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-heading: var(--font-sans);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-group(.animate-in){
|
||||
animation: animate-in ease-in 0.5s;
|
||||
}
|
||||
|
||||
::view-transition-group(.animate-out){
|
||||
animation: animate-out ease-in 0.5s;
|
||||
}
|
||||
65
webapp/app/layout.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
// import React from "react";
|
||||
|
||||
// export default function RootLayoutClient({ children }) {
|
||||
// React.useEffect(() => {
|
||||
// if ("serviceWorker" in navigator) {
|
||||
// navigator.serviceWorker
|
||||
// .register("/service-worker.js")
|
||||
// .then((registration) => {
|
||||
// console.log("Service Worker registered with scope:", registration.scope);
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.error("Service Worker registration failed:", error);
|
||||
// });
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// return (
|
||||
// <div className="text-white flex flex-col">
|
||||
// <div className="container mx-auto px-4 max-w-[1024px]">
|
||||
// {children}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import React from "react";
|
||||
import RootLayoutClient from "./layoutClient";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "CoffeeChat",
|
||||
description: "Start chatting with your communities",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased h-full w-full`}
|
||||
>
|
||||
<RootLayoutClient>
|
||||
{children}
|
||||
</RootLayoutClient>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
47
webapp/app/layoutClient.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
// import React from "react";
|
||||
|
||||
// export default function RootLayoutClient({ children }) {
|
||||
// React.useEffect(() => {
|
||||
// if ("serviceWorker" in navigator) {
|
||||
// navigator.serviceWorker
|
||||
// .register("/service-worker.js")
|
||||
// .then((registration) => {
|
||||
// console.log("Service Worker registered with scope:", registration.scope);
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.error("Service Worker registration failed:", error);
|
||||
// });
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// return (
|
||||
// <div className="text-white flex flex-col">
|
||||
// <div className="container mx-auto px-4 max-w-[1024px]">
|
||||
// {children}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
export default function RootLayoutClient({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
React.useEffect(() => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then((registration) => {
|
||||
console.log("Service Worker registered with scope:", registration.scope);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Service Worker registration failed:", error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
return (children);
|
||||
}
|
||||
11
webapp/app/login/page.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { LoginForm } from "@/components/login-form"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
81
webapp/app/manifest.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "CoffeeChat",
|
||||
short_name: "CoffeeChat",
|
||||
start_url: "chat.rootbranch.co.za",
|
||||
display: "standalone",
|
||||
description: "Start chatting with your communities",
|
||||
lang: " en",
|
||||
dir: "auto",
|
||||
theme_color: "#59168b",
|
||||
background_color: "#59168b",
|
||||
orientation: "any",
|
||||
icons: [
|
||||
{
|
||||
src: "/manifest-icon-192.maskable.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/manifest-icon-192.maskable.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
{
|
||||
src: "/manifest-icon-512.maskable.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/manifest-icon-512.maskable.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
screenshots: [
|
||||
{
|
||||
src: "screen1.png",
|
||||
sizes: "2880x1800",
|
||||
type: "image/png",
|
||||
form_factor: "wide",
|
||||
label: "Communities Home Page",
|
||||
},
|
||||
{
|
||||
src: "screen2.png",
|
||||
sizes: "570x1265",
|
||||
type: "image/png",
|
||||
form_factor: "narrow",
|
||||
label: "Communities Home Page",
|
||||
},
|
||||
],
|
||||
related_applications: [
|
||||
{
|
||||
platform: "windows",
|
||||
url: "https://chat.rootbranch.co.za",
|
||||
},
|
||||
],
|
||||
prefer_related_applications: false,
|
||||
shortcuts: [
|
||||
{
|
||||
name: "CoffeeChat",
|
||||
url: "chat.rootbranch.co.za",
|
||||
description: "Start chatting with your communities",
|
||||
icons: [
|
||||
{
|
||||
src: "manifest-icon-96.maskable.png",
|
||||
purpose: "any",
|
||||
sizes: "96x96",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
display_override: ["fullscreen"],
|
||||
};
|
||||
}
|
||||
56
webapp/app/page.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { LoginFormAction } from "@/actions/auth/AuthActions";
|
||||
import { getRooms } from "@/actions/room/RoomActions";
|
||||
import RoomCard from "@/components/RoomCard";
|
||||
import { Handshake } from "lucide-react";
|
||||
import { cookies } from "next/headers";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ViewTransition } from "react";
|
||||
|
||||
export default async function Home() {
|
||||
|
||||
const cj = await cookies()
|
||||
const session = cj.get(`ccsession`)
|
||||
|
||||
const rooms = await getRooms()
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-black font-sans">
|
||||
<form
|
||||
className="flex flex-col lg:flex-row p-2 w-full bg-purple-900 text-white shadow-sm shadow-purple-400 z-20"
|
||||
action={LoginFormAction}>
|
||||
<div className="grow flex">
|
||||
<Handshake size={30} className="me-2" />
|
||||
<input className=" w-full focus:p-2 transition-all" placeholder="Find" />
|
||||
</div>
|
||||
{/* <input type="email" name="email" placeholder="Email" />
|
||||
<input type="password" name="password" placeholder="Password" /> */}
|
||||
{!session &&
|
||||
<div className="flex">
|
||||
<Link className="px-2" href={`/login`}>Login</Link>
|
||||
<ViewTransition name="register-button" exit={'animate-out'}>
|
||||
<Link className="px-2" href={`/register`}>Register</Link>
|
||||
</ViewTransition>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 m-2">
|
||||
{rooms.map((room: {
|
||||
title: string;
|
||||
room: string;
|
||||
image: any;
|
||||
color: string;
|
||||
}, i: number) =>
|
||||
<RoomCard
|
||||
key={`Card-${i}`}
|
||||
color={room.color}
|
||||
image={room.image}
|
||||
room={room.room}
|
||||
title={room.title}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* {JSON.stringify(rooms)} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
webapp/app/register/error.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
'use client'
|
||||
import ErrorDialog from '@/components/Dialogs/ErrorDialog'
|
||||
import React from 'react'
|
||||
|
||||
export default function error({ error }: { error: { message: string } }) {
|
||||
return (
|
||||
<ErrorDialog error={JSON.parse(error.message).error} />
|
||||
// <div>error : {JSON.stringify(JSON.parse(error.message).error)}</div>
|
||||
)
|
||||
}
|
||||
57
webapp/app/register/page.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { RegisterFormAction } from '@/actions/auth/AuthActions'
|
||||
import Link from 'next/link'
|
||||
import React, { ViewTransition } from 'react'
|
||||
// import { ViewTransition } from 'react'
|
||||
export default function Register() {
|
||||
return (<ViewTransition enter={'auto'} exit={'auto'} default={'auto'}>
|
||||
|
||||
<div className='bg-black flex flex-col'>
|
||||
<div
|
||||
className='bg-white/10 h-svh max-w-[100vw] overflow-hidden backdrop-blur-lg flex flex-col border border-white/20 shadow-lg'
|
||||
>
|
||||
|
||||
<div className='bg-purple-950 text-white flex justify-evenly p-2 font-bold'>
|
||||
<div>Register Your Profile</div>
|
||||
</div>
|
||||
<form action={RegisterFormAction} className='m-2 flex flex-col grow'>
|
||||
<div className='grow'>
|
||||
<div className='text-white'>
|
||||
<div className='flex'>
|
||||
<label className='me-2 w-1/2'>Your name</label>
|
||||
<input placeholder='Your name' type='text' name='name' className='grow' />
|
||||
</div>
|
||||
<small style={{ fontSize: '0.5rem' }} className='text-xs text-justify '>This will be used for your records but aliases are allowed while using chats</small>
|
||||
</div>
|
||||
<hr />
|
||||
<div className='text-white'>
|
||||
<div className='flex'>
|
||||
<label className='me-2 w-1/2'>Email</label>
|
||||
<input placeholder='Email' type='email' name='email' className='grow' />
|
||||
</div>
|
||||
<small style={{ fontSize: '0.5rem' }} className='text-xs text-justify '>This will be used to login to the system and gain access to restricted groups</small>
|
||||
</div>
|
||||
<hr />
|
||||
<div className='text-white'>
|
||||
<div className='flex'>
|
||||
<label className='me-2 w-1/2'>Password</label>
|
||||
<input placeholder='Password' type='password' name='password' className='grow' />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
<div className='flex w-full justify-between p-2'>
|
||||
<Link href={`/`}><button type='button' className='self-end bg-purple-900 p-2 text-white'>Cancel</button></Link>
|
||||
<ViewTransition name='register-button' enter={'animate-in'}>
|
||||
<button className='self-end bg-purple-900 p-2 text-white'>Register</button>
|
||||
</ViewTransition>
|
||||
</div>
|
||||
{/*
|
||||
|
||||
</div>
|
||||
*/}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ViewTransition>
|
||||
)
|
||||
}
|
||||
11
webapp/app/signup/page.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { SignupForm } from "@/components/signup-form"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<SignupForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
10
webapp/app/tesst/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import MediaSoupWidget from '@/components/chat/MediaSoupWidget'
|
||||
import WebRTCChat from '@/components/chat/WebRTCChatUsingPeer'
|
||||
import React from 'react'
|
||||
|
||||
export default function Tttest() {
|
||||
return (
|
||||
<div><WebRTCChat /></div>
|
||||
// <div><MediaSoupWidget /></div>
|
||||
)
|
||||
}
|
||||
16
webapp/app/testb/page.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import ChatNoServerWidget from '@/components/chat/ChatNoServerWidget'
|
||||
import ChatWS from '@/components/chat/ChatWS'
|
||||
import MediaSoupWidget from '@/components/chat/MediaSoupWidget'
|
||||
import WebRTCChatUsingPeer from '@/components/chat/WebRTCChatUsingPeer'
|
||||
import WebRTCChat from '@/components/chat/WebRTCChatUsingPeer'
|
||||
import WebRTComponent from '@/components/chat/WebRTComponent'
|
||||
import React from 'react'
|
||||
|
||||
export default function Tttest() {
|
||||
return (
|
||||
// <div><WebRTCChat /></div>
|
||||
// <div><ChatNoServerWidget /></div>
|
||||
// <div><WebRTCChatUsingPeer /></div>
|
||||
<div><ChatWS /></div>
|
||||
)
|
||||
}
|
||||
33
webapp/app/testc/page.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
export default function Search() {
|
||||
return (
|
||||
<div className='w-screen h-screen'
|
||||
style={{ 'background': 'conic-gradient(from 210deg, #c6bcb9 0.000deg, #c6bcb9 24.000deg, #b9b6b9 calc(24.000deg + 0.1deg), #b9b6b9 48.000deg, #aab0b8 calc(48.000deg + 0.1deg), #aab0b8 72.000deg, #9ba9b6 calc(72.000deg + 0.1deg), #9ba9b6 96.000deg, #8ca2b4 calc(96.000deg + 0.1deg), #8ca2b4 120.000deg, #7e99b0 calc(120.000deg + 0.1deg), #7e99b0 144.000deg, #7191ac calc(144.000deg + 0.1deg), #7191ac 168.000deg, #6688a7 calc(168.000deg + 0.1deg), #6688a7 192.000deg, #5d7fa2 calc(192.000deg + 0.1deg), #5d7fa2 216.000deg, #56769c calc(216.000deg + 0.1deg), #56769c 240.000deg, #526d95 calc(240.000deg + 0.1deg), #526d95 264.000deg, #50648e calc(264.000deg + 0.1deg), #50648e 288.000deg, #515b86 calc(288.000deg + 0.1deg), #515b86 312.000deg, #55547e calc(312.000deg + 0.1deg), #55547e 336.000deg, #5c4c75 calc(336.000deg + 0.1deg) 360.000deg)' }}
|
||||
|
||||
>
|
||||
<div className='bg-purple-900 flex p-2'>
|
||||
<input placeholder='Find' className='text-2xl grow bg-white' />
|
||||
|
||||
</div>
|
||||
<div className='grid grid-cols-3'>
|
||||
<RoomCard title={'Dating'} room={"123"} />
|
||||
<RoomCard title={'Business'} room={"123"} />
|
||||
<RoomCard title={'Programming'} room={"123"} />
|
||||
<RoomCard title={'Somthing Else'} room={"123"} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function RoomCard({
|
||||
title,
|
||||
room,
|
||||
}: {
|
||||
title: string
|
||||
room: string
|
||||
}) {
|
||||
return <Link href={`/testb?room=${room}`}><div>{title}</div></Link>
|
||||
}
|
||||
98
webapp/app/testd/page.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import RoomCard from '@/components/RoomCard'
|
||||
import { CallApi } from '@/helper/api/ApiConnector'
|
||||
import Link from 'next/link'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function Search() {
|
||||
const [details, setDetails] = useState<{
|
||||
id?: number;
|
||||
image: any,
|
||||
title: string,
|
||||
room: string,
|
||||
color: string
|
||||
}>()
|
||||
|
||||
const handleSave = async () => {
|
||||
console.log(await CallApi(`/room`, 'POST', { id: 0, ...details,image: details?.image }))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-screen h-screen bg-black'
|
||||
// style={{ 'background': 'conic-gradient(from 210deg, #c6bcb9 0.000deg, #c6bcb9 24.000deg, #b9b6b9 calc(24.000deg + 0.1deg), #b9b6b9 48.000deg, #aab0b8 calc(48.000deg + 0.1deg), #aab0b8 72.000deg, #9ba9b6 calc(72.000deg + 0.1deg), #9ba9b6 96.000deg, #8ca2b4 calc(96.000deg + 0.1deg), #8ca2b4 120.000deg, #7e99b0 calc(120.000deg + 0.1deg), #7e99b0 144.000deg, #7191ac calc(144.000deg + 0.1deg), #7191ac 168.000deg, #6688a7 calc(168.000deg + 0.1deg), #6688a7 192.000deg, #5d7fa2 calc(192.000deg + 0.1deg), #5d7fa2 216.000deg, #56769c calc(216.000deg + 0.1deg), #56769c 240.000deg, #526d95 calc(240.000deg + 0.1deg), #526d95 264.000deg, #50648e calc(264.000deg + 0.1deg), #50648e 288.000deg, #515b86 calc(288.000deg + 0.1deg), #515b86 312.000deg, #55547e calc(312.000deg + 0.1deg), #55547e 336.000deg, #5c4c75 calc(336.000deg + 0.1deg) 360.000deg)' }}
|
||||
|
||||
>
|
||||
<div className='grow flex flex-col items-center align-middle justify-center h-full w-full'>
|
||||
|
||||
<div
|
||||
className='bg-white/10 w-full h-full lg:h-[90%] lg:w-[90%] m-2 backdrop-blur-lg flex flex-col border border-white/20 shadow-lg'
|
||||
>
|
||||
|
||||
<div className='bg-purple-950 text-white flex justify-evenly'>
|
||||
<button className='p-2 grow hover:backdrop-blur-lg hover:bg-white/10'>Room Details</button>
|
||||
<button className='p-2 grow hover:backdrop-blur-lg hover:bg-white/10'>Audiance</button>
|
||||
<button className='p-2 grow hover:backdrop-blur-lg hover:bg-white/10'>Rules</button>
|
||||
<button className='p-2 grow hover:backdrop-blur-lg hover:bg-white/10'>Guest List</button>
|
||||
</div>
|
||||
<div>
|
||||
<input placeholder='Room Name'
|
||||
onChange={e => setDetails((d: any) => {
|
||||
return { ...d, title: e.target.value }
|
||||
})}
|
||||
className='text-white w-full border-0 focus:p-2 transition-all focus:border-0' />
|
||||
|
||||
<button onClick={handleSave} className='text-white'>Save</button>
|
||||
<div>
|
||||
<label>Image</label>
|
||||
<input
|
||||
onChange={e => {
|
||||
const file = e.target.files![0]
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
const reader = new FileReader(); // Create a new FileReader instance
|
||||
|
||||
// Set up the onload event for the reader
|
||||
reader.onload = function (e) {
|
||||
// e.target.result contains the image data as a Base64 encoded URL (data URL)
|
||||
console.log('Image Data URL:', e.target!.result);
|
||||
|
||||
// You can now set the src of an image tag to this data URL to display it
|
||||
setDetails((d: any) => {
|
||||
return { ...d, image: e.target!.result }
|
||||
})
|
||||
// imagePreview.src = e.target.result;
|
||||
|
||||
// To get pixel data (ImageData object), you would use a canvas element
|
||||
// (See "Getting Pixel Data" below)
|
||||
};
|
||||
|
||||
// Read the image file as a Data URL
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
// imagePreview.src = '#'; // Clear preview if not an image
|
||||
console.error('Please select a valid image file.');
|
||||
}
|
||||
}}
|
||||
className='text-white' type='file' name='image' />
|
||||
</div>
|
||||
<div>
|
||||
<label>Colour</label>
|
||||
<input
|
||||
onChange={e => setDetails((d: any) => {
|
||||
return { ...d, color: e.target.value }
|
||||
})}
|
||||
type='color' name='color' />
|
||||
</div>
|
||||
{details && <RoomCard
|
||||
color={details.color}
|
||||
image={details!.image}
|
||||
room='TEST'
|
||||
title={details.title}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
38
webapp/app/teste/page.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
export default function Profile() {
|
||||
return (
|
||||
<div className='w-screen h-screen bg-black'
|
||||
// style={{ 'background': 'conic-gradient(from 210deg, #c6bcb9 0.000deg, #c6bcb9 24.000deg, #b9b6b9 calc(24.000deg + 0.1deg), #b9b6b9 48.000deg, #aab0b8 calc(48.000deg + 0.1deg), #aab0b8 72.000deg, #9ba9b6 calc(72.000deg + 0.1deg), #9ba9b6 96.000deg, #8ca2b4 calc(96.000deg + 0.1deg), #8ca2b4 120.000deg, #7e99b0 calc(120.000deg + 0.1deg), #7e99b0 144.000deg, #7191ac calc(144.000deg + 0.1deg), #7191ac 168.000deg, #6688a7 calc(168.000deg + 0.1deg), #6688a7 192.000deg, #5d7fa2 calc(192.000deg + 0.1deg), #5d7fa2 216.000deg, #56769c calc(216.000deg + 0.1deg), #56769c 240.000deg, #526d95 calc(240.000deg + 0.1deg), #526d95 264.000deg, #50648e calc(264.000deg + 0.1deg), #50648e 288.000deg, #515b86 calc(288.000deg + 0.1deg), #515b86 312.000deg, #55547e calc(312.000deg + 0.1deg), #55547e 336.000deg, #5c4c75 calc(336.000deg + 0.1deg) 360.000deg)' }}
|
||||
|
||||
>
|
||||
<div className='grow flex flex-col items-center align-middle justify-center h-full w-full'>
|
||||
|
||||
<div
|
||||
className='bg-white/10 w-full h-full lg:h-[90%] lg:w-[90%] m-2 backdrop-blur-lg flex flex-col border border-white/20 shadow-lg'
|
||||
>
|
||||
|
||||
<div className='bg-purple-950 text-white flex justify-evenly'>
|
||||
<button className='p-2 grow hover:backdrop-blur-lg hover:bg-white/10'>Room Details</button>
|
||||
<button className='p-2 grow hover:backdrop-blur-lg hover:bg-white/10'>Audiance</button>
|
||||
<button className='p-2 grow hover:backdrop-blur-lg hover:bg-white/10'>Rules</button>
|
||||
<button className='p-2 grow hover:backdrop-blur-lg hover:bg-white/10'>Guest List</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function RoomCard({
|
||||
title,
|
||||
room,
|
||||
}: {
|
||||
title: string
|
||||
room: string
|
||||
}) {
|
||||
return <Link href={`/testb?room=${room}`}><div>{title}</div></Link>
|
||||
}
|
||||
25
webapp/components.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "radix-nova",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
7
webapp/components/Dialogs/ErrorDialog.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function ErrorDialog({ error }: { error: string | Error }) {
|
||||
return (
|
||||
<div>{error instanceof Error ? error.message : error }</div>
|
||||
)
|
||||
}
|
||||
59
webapp/components/RoomCard.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import Link from "next/link"
|
||||
|
||||
export default function RoomCard({
|
||||
title,
|
||||
room,
|
||||
image,
|
||||
color,
|
||||
}: {
|
||||
title: string
|
||||
room: string
|
||||
image: any,
|
||||
color: string
|
||||
}) {
|
||||
if(!image) return "No Image Available"
|
||||
return <Link href={`/testb?room=${room}`}>
|
||||
{image && <div
|
||||
style={{
|
||||
// minWidth: 500,
|
||||
width: '100%',
|
||||
height: 275,
|
||||
overflow: 'hidden',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center'
|
||||
}}
|
||||
className='relative opacity-70 hover:opacity-90'
|
||||
>
|
||||
|
||||
<div className='text-2xl text-center w-full text-shadow-black text-shadow-2xs text-white bg-transparent absolute z-20 top-0'>{title}</div>
|
||||
<img
|
||||
width={'100%'}
|
||||
height={275}
|
||||
style={{
|
||||
filter: 'grayscale(100%)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
src={image}
|
||||
className='absolute top-0'
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
opacity: '50%',
|
||||
// backgroundColor: 'rgba(100,27,198,0.5',
|
||||
// width: 500,
|
||||
width: '100%',
|
||||
height: 275
|
||||
}}
|
||||
className='absolute top-0 bottom-0 left-0 right-0'
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
</Link>
|
||||
}
|
||||
33
webapp/components/chat/ChatBox.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import { Input } from '../ui/input'
|
||||
import { InputGroup, InputGroupAddon, InputGroupButton } from '../ui/input-group'
|
||||
import { Textarea } from '../ui/textarea'
|
||||
|
||||
export default function ChatBox() {
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<div className='grow flex flex-col'>
|
||||
CHAT MESSAGES
|
||||
<div className='p-1 rounded-2xl border max-w-1/2 self-start'>Placeholder message</div>
|
||||
<div className='p-1 rounded-2xl border max-w-1/2 self-end'>Placeholder message</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<InputGroup>
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className="flex field-sizing-content min-h-16 w-full resize-none rounded-md bg-transparent px-3 py-2.5 text-base transition-[color,box-shadow] outline-none md:text-sm"
|
||||
placeholder="Message..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton className="ml-auto" size="sm" variant="default">
|
||||
Send
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
175
webapp/components/chat/ChatNoServerWidget copy.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
export default function ChatNoServerWidget() {
|
||||
const localVideo = useRef<HTMLVideoElement | null>(null)
|
||||
const remoteVideo = useRef<HTMLVideoElement | null>(null)
|
||||
const localStream = useRef<MediaStream>(null)
|
||||
const pc1 = useRef<RTCPeerConnection>(null)
|
||||
const pc2 = useRef<RTCPeerConnection>(null)
|
||||
|
||||
|
||||
const handleStart = async () => {
|
||||
console.log('Requesting local stream');
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
console.log('Received local stream');
|
||||
localVideo.current!.srcObject = stream;
|
||||
localStream.current = stream;
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
alert(`getUserMedia() error: ${(e as Error).name}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getName(pc: RTCPeerConnection) {
|
||||
return (pc === pc1.current) ? 'pc1' : 'pc2';
|
||||
}
|
||||
|
||||
function getOtherPc(pc: RTCPeerConnection) {
|
||||
return (pc === pc1.current) ? pc2.current : pc1.current;
|
||||
}
|
||||
|
||||
async function onIceCandidate(pc: RTCPeerConnection, event: RTCPeerConnectionIceEvent) {
|
||||
try {
|
||||
await (getOtherPc(pc)!.addIceCandidate(event.candidate));
|
||||
onAddIceCandidateSuccess(pc);
|
||||
} catch (e) {
|
||||
onAddIceCandidateError(pc, e);
|
||||
}
|
||||
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
|
||||
}
|
||||
function onAddIceCandidateSuccess(pc: RTCPeerConnection) {
|
||||
console.log(`${getName(pc)} addIceCandidate success`);
|
||||
}
|
||||
|
||||
function onAddIceCandidateError(pc: RTCPeerConnection, error: any) {
|
||||
console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);
|
||||
}
|
||||
|
||||
function onIceStateChange(pc: RTCPeerConnection, event: Event) {
|
||||
if (pc) {
|
||||
console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`);
|
||||
console.log('ICE state change event: ', event);
|
||||
}
|
||||
}
|
||||
|
||||
function gotRemoteStream(e: RTCTrackEvent) {
|
||||
if (remoteVideo.current!.srcObject !== e.streams[0]) {
|
||||
remoteVideo.current!.srcObject = e.streams[0];
|
||||
console.log('pc2 received remote stream');
|
||||
}
|
||||
}
|
||||
async function call() {
|
||||
console.log('Starting call');
|
||||
|
||||
const videoTracks = localStream.current!.getVideoTracks();
|
||||
const audioTracks = localStream.current!.getAudioTracks();
|
||||
if (videoTracks.length > 0) {
|
||||
console.log(`Using video device: ${videoTracks[0].label}`);
|
||||
}
|
||||
if (audioTracks.length > 0) {
|
||||
console.log(`Using audio device: ${audioTracks[0].label}`);
|
||||
}
|
||||
const configuration = {};
|
||||
console.log('RTCPeerConnection configuration:', configuration);
|
||||
pc1.current = new RTCPeerConnection(configuration);
|
||||
console.log('Created local peer connection object pc1');
|
||||
pc1.current.addEventListener('icecandidate', e => onIceCandidate(pc1.current!, e));
|
||||
pc2.current = new RTCPeerConnection(configuration);
|
||||
console.log('Created remote peer connection object pc2');
|
||||
pc2.current.addEventListener('icecandidate', e => onIceCandidate(pc2.current!, e));
|
||||
pc1.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1.current!, e));
|
||||
pc2.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2.current!, e));
|
||||
pc2.current.addEventListener('track', gotRemoteStream);
|
||||
|
||||
localStream.current!.getTracks().forEach(track => pc1.current!.addTrack(track, localStream.current!));
|
||||
console.log('Added local stream to pc1');
|
||||
|
||||
try {
|
||||
console.log('pc1 createOffer start');
|
||||
const offer = await pc1.current.createOffer();
|
||||
// const offer = await pc1.current.createOffer(offerOptions);
|
||||
await onCreateOfferSuccess(offer);
|
||||
} catch (e) {
|
||||
onCreateSessionDescriptionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onSetLocalSuccess(pc: RTCPeerConnection) {
|
||||
console.log(`${getName(pc)} setLocalDescription complete`);
|
||||
}
|
||||
|
||||
function onSetRemoteSuccess(pc: RTCPeerConnection) {
|
||||
console.log(`${getName(pc)} setRemoteDescription complete`);
|
||||
}
|
||||
|
||||
function onSetSessionDescriptionError(error: any) {
|
||||
console.log(`Failed to set session description: ${error.toString()}`);
|
||||
}
|
||||
|
||||
async function onCreateOfferSuccess(desc: RTCSessionDescriptionInit) {
|
||||
console.log(`Offer from pc1\n${desc.sdp}`);
|
||||
console.log('pc1 setLocalDescription start');
|
||||
try {
|
||||
await pc1.current!.setLocalDescription(desc);
|
||||
onSetLocalSuccess(pc1.current!);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
|
||||
console.log('pc2 setRemoteDescription start');
|
||||
try {
|
||||
await pc2.current!.setRemoteDescription(desc);
|
||||
onSetRemoteSuccess(pc2.current!);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
|
||||
console.log('pc2 createAnswer start');
|
||||
// Since the 'remote' side has no media stream we need
|
||||
// to pass in the right constraints in order for it to
|
||||
// accept the incoming offer of audio and video.
|
||||
try {
|
||||
const answer = await pc2.current!.createAnswer();
|
||||
await onCreateAnswerSuccess(answer);
|
||||
} catch (e) {
|
||||
onCreateSessionDescriptionError(e);
|
||||
}
|
||||
}
|
||||
async function onCreateAnswerSuccess(desc: RTCSessionDescriptionInit) {
|
||||
console.log(`Answer from pc2:\n${desc.sdp}`);
|
||||
console.log('pc2 setLocalDescription start');
|
||||
try {
|
||||
await pc2.current!.setLocalDescription(desc);
|
||||
onSetLocalSuccess(pc2.current!);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
console.log('pc1 setRemoteDescription start');
|
||||
try {
|
||||
await pc1.current!.setRemoteDescription(desc);
|
||||
onSetRemoteSuccess(pc1.current!);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onCreateSessionDescriptionError(error: any) {
|
||||
console.log(`Failed to create session description: ${error.toString()}`);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<video ref={localVideo} id="localVideo" playsInline autoPlay muted></video>
|
||||
<video ref={remoteVideo} id="remoteVideo" playsInline autoPlay></video>
|
||||
|
||||
<div className="box">
|
||||
<button onClick={handleStart} id="startButton">Start</button>
|
||||
<button onClick={() => call()} id="callButton">Call</button>
|
||||
<button id="hangupButton">Hang Up</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
175
webapp/components/chat/ChatNoServerWidget.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
export default function ChatNoServerWidget() {
|
||||
const localVideo = useRef<HTMLVideoElement | null>(null)
|
||||
const remoteVideo = useRef<HTMLVideoElement | null>(null)
|
||||
const localStream = useRef<MediaStream>(null)
|
||||
const pc1 = useRef<RTCPeerConnection>(null)
|
||||
const pc2 = useRef<RTCPeerConnection>(null)
|
||||
|
||||
|
||||
const handleStart = async () => {
|
||||
console.log('Requesting local stream');
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
console.log('Received local stream');
|
||||
localVideo.current!.srcObject = stream;
|
||||
localStream.current = stream;
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
alert(`getUserMedia() error: ${(e as Error).name}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getName(pc: RTCPeerConnection) {
|
||||
return (pc === pc1.current) ? 'pc1' : 'pc2';
|
||||
}
|
||||
|
||||
function getOtherPc(pc: RTCPeerConnection) {
|
||||
return (pc === pc1.current) ? pc2.current : pc1.current;
|
||||
}
|
||||
|
||||
async function onIceCandidate(pc: RTCPeerConnection, event: RTCPeerConnectionIceEvent) {
|
||||
try {
|
||||
await (getOtherPc(pc)!.addIceCandidate(event.candidate));
|
||||
onAddIceCandidateSuccess(pc);
|
||||
} catch (e) {
|
||||
onAddIceCandidateError(pc, e);
|
||||
}
|
||||
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
|
||||
}
|
||||
function onAddIceCandidateSuccess(pc: RTCPeerConnection) {
|
||||
console.log(`${getName(pc)} addIceCandidate success`);
|
||||
}
|
||||
|
||||
function onAddIceCandidateError(pc: RTCPeerConnection, error: any) {
|
||||
console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);
|
||||
}
|
||||
|
||||
function onIceStateChange(pc: RTCPeerConnection, event: Event) {
|
||||
if (pc) {
|
||||
console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`);
|
||||
console.log('ICE state change event: ', event);
|
||||
}
|
||||
}
|
||||
|
||||
function gotRemoteStream(e: RTCTrackEvent) {
|
||||
if (remoteVideo.current!.srcObject !== e.streams[0]) {
|
||||
remoteVideo.current!.srcObject = e.streams[0];
|
||||
console.log('pc2 received remote stream');
|
||||
}
|
||||
}
|
||||
async function call() {
|
||||
console.log('Starting call');
|
||||
|
||||
const videoTracks = localStream.current!.getVideoTracks();
|
||||
const audioTracks = localStream.current!.getAudioTracks();
|
||||
if (videoTracks.length > 0) {
|
||||
console.log(`Using video device: ${videoTracks[0].label}`);
|
||||
}
|
||||
if (audioTracks.length > 0) {
|
||||
console.log(`Using audio device: ${audioTracks[0].label}`);
|
||||
}
|
||||
const configuration = {};
|
||||
console.log('RTCPeerConnection configuration:', configuration);
|
||||
pc1.current = new RTCPeerConnection(configuration);
|
||||
console.log('Created local peer connection object pc1');
|
||||
pc1.current.addEventListener('icecandidate', e => onIceCandidate(pc1.current!, e));
|
||||
pc2.current = new RTCPeerConnection(configuration);
|
||||
console.log('Created remote peer connection object pc2');
|
||||
pc2.current.addEventListener('icecandidate', e => onIceCandidate(pc2.current!, e));
|
||||
pc1.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1.current!, e));
|
||||
pc2.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2.current!, e));
|
||||
pc2.current.addEventListener('track', gotRemoteStream);
|
||||
|
||||
localStream.current!.getTracks().forEach(track => pc1.current!.addTrack(track, localStream.current!));
|
||||
console.log('Added local stream to pc1');
|
||||
|
||||
try {
|
||||
console.log('pc1 createOffer start');
|
||||
const offer = await pc1.current.createOffer();
|
||||
// const offer = await pc1.current.createOffer(offerOptions);
|
||||
await onCreateOfferSuccess(offer);
|
||||
} catch (e) {
|
||||
onCreateSessionDescriptionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onSetLocalSuccess(pc: RTCPeerConnection) {
|
||||
console.log(`${getName(pc)} setLocalDescription complete`);
|
||||
}
|
||||
|
||||
function onSetRemoteSuccess(pc: RTCPeerConnection) {
|
||||
console.log(`${getName(pc)} setRemoteDescription complete`);
|
||||
}
|
||||
|
||||
function onSetSessionDescriptionError(error: any) {
|
||||
console.log(`Failed to set session description: ${error.toString()}`);
|
||||
}
|
||||
|
||||
async function onCreateOfferSuccess(desc: RTCSessionDescriptionInit) {
|
||||
console.log(`Offer from pc1\n${desc.sdp}`);
|
||||
console.log('pc1 setLocalDescription start');
|
||||
try {
|
||||
await pc1.current!.setLocalDescription(desc);
|
||||
onSetLocalSuccess(pc1.current!);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
|
||||
console.log('pc2 setRemoteDescription start');
|
||||
try {
|
||||
await pc2.current!.setRemoteDescription(desc);
|
||||
onSetRemoteSuccess(pc2.current!);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
|
||||
console.log('pc2 createAnswer start');
|
||||
// Since the 'remote' side has no media stream we need
|
||||
// to pass in the right constraints in order for it to
|
||||
// accept the incoming offer of audio and video.
|
||||
try {
|
||||
const answer = await pc2.current!.createAnswer();
|
||||
await onCreateAnswerSuccess(answer);
|
||||
} catch (e) {
|
||||
onCreateSessionDescriptionError(e);
|
||||
}
|
||||
}
|
||||
async function onCreateAnswerSuccess(desc: RTCSessionDescriptionInit) {
|
||||
console.log(`Answer from pc2:\n${desc.sdp}`);
|
||||
console.log('pc2 setLocalDescription start');
|
||||
try {
|
||||
await pc2.current!.setLocalDescription(desc);
|
||||
onSetLocalSuccess(pc2.current!);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
console.log('pc1 setRemoteDescription start');
|
||||
try {
|
||||
await pc1.current!.setRemoteDescription(desc);
|
||||
onSetRemoteSuccess(pc1.current!);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onCreateSessionDescriptionError(error: any) {
|
||||
console.log(`Failed to create session description: ${error.toString()}`);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<video ref={localVideo} id="localVideo" playsInline autoPlay muted></video>
|
||||
<video ref={remoteVideo} id="remoteVideo" playsInline autoPlay></video>
|
||||
|
||||
<div className="box">
|
||||
<button onClick={handleStart} id="startButton">Start</button>
|
||||
<button onClick={() => call()} id="callButton">Call</button>
|
||||
<button id="hangupButton">Hang Up</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
134
webapp/components/chat/ChatWS.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Socket } from 'socket.io';
|
||||
import { io } from 'socket.io-client';
|
||||
import { socket } from './socket';
|
||||
import Link from 'next/link';
|
||||
|
||||
// https://www.baeldung.com/webrtc
|
||||
export default function ChatWS() {
|
||||
|
||||
const [session, setSession] = useState<{
|
||||
who: string,
|
||||
room?: string
|
||||
}>()
|
||||
const [messages, setMessages] = useState<{
|
||||
who: string,
|
||||
msg: string
|
||||
}[]>([])
|
||||
const nameRef = useRef<HTMLInputElement | null>(null)
|
||||
useEffect(() => {
|
||||
socket.onAny((ev, data) => {
|
||||
console.log(`ANY`, ev, data)
|
||||
// setMessages(prevMessages => [...prevMessages, data]);
|
||||
});
|
||||
socket.on('chat message', (data) => {
|
||||
// console.log(`chat message`,data)
|
||||
setMessages(prevMessages => [...prevMessages, data]);
|
||||
});
|
||||
|
||||
// Clean up the listener when the component unmounts
|
||||
return () => {
|
||||
socket.off('chat message');
|
||||
};
|
||||
}, [])
|
||||
|
||||
if (!session?.who) return <div
|
||||
style={{ 'background': 'conic-gradient(from 210deg, #c6bcb9 0.000deg, #c6bcb9 24.000deg, #b9b6b9 calc(24.000deg + 0.1deg), #b9b6b9 48.000deg, #aab0b8 calc(48.000deg + 0.1deg), #aab0b8 72.000deg, #9ba9b6 calc(72.000deg + 0.1deg), #9ba9b6 96.000deg, #8ca2b4 calc(96.000deg + 0.1deg), #8ca2b4 120.000deg, #7e99b0 calc(120.000deg + 0.1deg), #7e99b0 144.000deg, #7191ac calc(144.000deg + 0.1deg), #7191ac 168.000deg, #6688a7 calc(168.000deg + 0.1deg), #6688a7 192.000deg, #5d7fa2 calc(192.000deg + 0.1deg), #5d7fa2 216.000deg, #56769c calc(216.000deg + 0.1deg), #56769c 240.000deg, #526d95 calc(240.000deg + 0.1deg), #526d95 264.000deg, #50648e calc(264.000deg + 0.1deg), #50648e 288.000deg, #515b86 calc(288.000deg + 0.1deg), #515b86 312.000deg, #55547e calc(312.000deg + 0.1deg), #55547e 336.000deg, #5c4c75 calc(336.000deg + 0.1deg) 360.000deg)' }}
|
||||
className='bg-gray-100 h-svh w-screen z-30 fixed flex items-center justify-center align-middle'>
|
||||
<div
|
||||
// onSubmit={ev => {
|
||||
// ev.preventDefault()
|
||||
// const form = new FormData(ev.target)
|
||||
// alert(form.get(`name`))
|
||||
// setSession((s: any) => {
|
||||
// return {
|
||||
// ...s,
|
||||
// who: form.get(`name`)
|
||||
// }
|
||||
// })
|
||||
// }}
|
||||
className='bg-white/10 backdrop-blur-lg w-96 h-64 flex flex-col border border-white/20 shadow-lg p-2'>
|
||||
<div className='grow font-black text-center'>
|
||||
Enter a name to use in this chat
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<input
|
||||
ref={nameRef}
|
||||
className='w-full focus:p-2 transition-all focus:bg-white'
|
||||
autoFocus
|
||||
name='name'
|
||||
placeholder='Your name' />
|
||||
</div>
|
||||
<button type='button'
|
||||
onClick={() => {
|
||||
setSession((s: any) => {
|
||||
return {
|
||||
...s,
|
||||
who: nameRef.current?.value
|
||||
}
|
||||
})
|
||||
}} className='shadow p-2'>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div
|
||||
/* https://grabient.com/HQNgrAHANMYIxhgJjAFigWlgTkVsaM2AzHJsMRAAzlKrTBwhJRwVgv4gjnYDsfcsWzpQSYq2BVpUKkA?style=angularSwatches&angle=210&steps=15 */
|
||||
|
||||
// background: conic-gradient(from 210deg, #7c6f6f 0.000deg, #7c6f6f 24.000deg, #6e6b6d calc(24.000deg + 0.1deg), #6e6b6d 48.000deg, #5f666a calc(48.000deg + 0.1deg), #5f666a 72.000deg, #505f67 calc(72.000deg + 0.1deg), #505f67 96.000deg, #3f5862 calc(96.000deg + 0.1deg), #3f5862 120.000deg, #2f515e calc(120.000deg + 0.1deg), #2f515e 144.000deg, #204858 calc(144.000deg + 0.1deg), #204858 168.000deg, #123f52 calc(168.000deg + 0.1deg), #123f52 192.000deg, #05364b calc(192.000deg + 0.1deg), #05364b 216.000deg, #002c43 calc(216.000deg + 0.1deg), #002c43 240.000deg, #00233b calc(240.000deg + 0.1deg), #00233b 264.000deg, #001933 calc(264.000deg + 0.1deg), #001933 288.000deg, #000f2a calc(288.000deg + 0.1deg), #000f2a 312.000deg, #000621 calc(312.000deg + 0.1deg), #000621 336.000deg, #000017 calc(336.000deg + 0.1deg) 360.000deg);
|
||||
style={{ 'background': 'black' }}
|
||||
// style={{ 'background': 'conic-gradient(from 210deg, #c6bcb9 0.000deg, #c6bcb9 24.000deg, #b9b6b9 calc(24.000deg + 0.1deg), #b9b6b9 48.000deg, #aab0b8 calc(48.000deg + 0.1deg), #aab0b8 72.000deg, #9ba9b6 calc(72.000deg + 0.1deg), #9ba9b6 96.000deg, #8ca2b4 calc(96.000deg + 0.1deg), #8ca2b4 120.000deg, #7e99b0 calc(120.000deg + 0.1deg), #7e99b0 144.000deg, #7191ac calc(144.000deg + 0.1deg), #7191ac 168.000deg, #6688a7 calc(168.000deg + 0.1deg), #6688a7 192.000deg, #5d7fa2 calc(192.000deg + 0.1deg), #5d7fa2 216.000deg, #56769c calc(216.000deg + 0.1deg), #56769c 240.000deg, #526d95 calc(240.000deg + 0.1deg), #526d95 264.000deg, #50648e calc(264.000deg + 0.1deg), #50648e 288.000deg, #515b86 calc(288.000deg + 0.1deg), #515b86 312.000deg, #55547e calc(312.000deg + 0.1deg), #55547e 336.000deg, #5c4c75 calc(336.000deg + 0.1deg) 360.000deg)' }}
|
||||
// style={{ 'background': 'conic-gradient(from 210deg, #7c6f6f 0.000deg, #7c6f6f 24.000deg, #6e6b6d calc(24.000deg + 0.1deg), #6e6b6d 48.000deg, #5f666a calc(48.000deg + 0.1deg), #5f666a 72.000deg, #505f67 calc(72.000deg + 0.1deg), #505f67 96.000deg, #3f5862 calc(96.000deg + 0.1deg), #3f5862 120.000deg, #2f515e calc(120.000deg + 0.1deg), #2f515e 144.000deg, #204858 calc(144.000deg + 0.1deg), #204858 168.000deg, #123f52 calc(168.000deg + 0.1deg), #123f52 192.000deg, #05364b calc(192.000deg + 0.1deg), #05364b 216.000deg, #002c43 calc(216.000deg + 0.1deg), #002c43 240.000deg, #00233b calc(240.000deg + 0.1deg), #00233b 264.000deg, #001933 calc(264.000deg + 0.1deg), #001933 288.000deg, #000f2a calc(288.000deg + 0.1deg), #000f2a 312.000deg, #000621 calc(312.000deg + 0.1deg), #000621 336.000deg, #000017 calc(336.000deg + 0.1deg) 360.000deg)' }}
|
||||
|
||||
className='flex flex-col h-svh w-screen'>
|
||||
<div className='bg-purple-900 shadow-sm shadow-purple-800'>
|
||||
<Link href={`/`}>
|
||||
<button className='bg-purple-950 text-white p-2 shadow hover:bg-purple-700'>Close Chat</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='grow flex flex-col overflow-auto gap-2'>
|
||||
{messages.map((msg, i) =>
|
||||
<div
|
||||
className={`w-1/2 flex flex-col bg-gray-100 p-2 rounded-2xl shadow ${msg.who === session.who ? 'self-start' : 'self-end'}`}
|
||||
key={`message-${i}`}>
|
||||
<small>{msg.who}</small>
|
||||
{msg.msg}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* {JSON.stringify(messages)} */}
|
||||
|
||||
|
||||
<div className='text-center text-amber-600 bg-amber-100'>
|
||||
All message are removed once you leave this chat
|
||||
</div>
|
||||
<form onSubmit={ev => {
|
||||
ev.preventDefault()
|
||||
const form = new FormData(ev.target)
|
||||
if (!form.get('msg')) return
|
||||
socket.emit('chat message', {
|
||||
who: session!.who ?? 'Annonymous',
|
||||
msg: form.get('msg') as string
|
||||
})
|
||||
ev.currentTarget.reset()
|
||||
}}
|
||||
className='flex p-2 bg-gray-100 shadow-2xl opacity-90'
|
||||
>
|
||||
<input placeholder='Message' name='msg' className='grow' />
|
||||
<button
|
||||
className='shadow p-2 bg-gray-300'
|
||||
onClick={() => {
|
||||
// socket.emit('chat message', {
|
||||
// who: 'Clint',
|
||||
// msg: `Hello`
|
||||
// })
|
||||
// socket.send(`Hello MSG`)
|
||||
}
|
||||
}>Send</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
388
webapp/components/chat/MediaSoupWidget.tsx
Normal file
@ -0,0 +1,388 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
import { Device } from 'mediasoup-client'
|
||||
import { Transport } from "mediasoup-client/types"
|
||||
|
||||
const wsURL = 'ws://localhost:4000'
|
||||
let remoteStream
|
||||
export default function MediaSoupWidget() {
|
||||
|
||||
const socket = useRef<WebSocket | undefined>(undefined)
|
||||
const device = useRef<Device | undefined>(undefined)
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||
const videoBRef = useRef<HTMLVideoElement | null>(null)
|
||||
const consumerTransport = useRef<Transport | null>(null)
|
||||
const [connected, setConnected] = useState<boolean>(false)
|
||||
const [states, setStates] = useState<string[]>([])
|
||||
|
||||
const loadDevices = async (routerCapabilities: any) => {
|
||||
try {
|
||||
device.current = new Device()
|
||||
await device.current!.load({ routerRtpCapabilities: routerCapabilities })
|
||||
// console.log(`Supported : `, device)
|
||||
setConnected(true)
|
||||
} catch (e) {
|
||||
if ((e as any).name === 'UnsupportedErro') {
|
||||
console.log(`Not Supported`)
|
||||
}
|
||||
console.error(`Faield to create device:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
const getUserMedia = async (transport: any, isWebCam: boolean) => {
|
||||
if (!device.current?.canProduce('video')) {
|
||||
console.error(`Cant stream video`)
|
||||
return
|
||||
}
|
||||
let stream
|
||||
try {
|
||||
stream = isWebCam ? await navigator.mediaDevices.getUserMedia({
|
||||
video: true, audio: true
|
||||
}) : await navigator.mediaDevices.getDisplayMedia({ video: true })
|
||||
} catch (e) {
|
||||
console.error(`Unable to get device to stream`, e)
|
||||
}
|
||||
return stream
|
||||
}
|
||||
|
||||
const onProducerTransportCreated = async (producerDetails: any) => {
|
||||
// 5. Createe Send Transport
|
||||
|
||||
setStates(s => [...s, 'createSendTransport'])
|
||||
const transport = device.current?.createSendTransport({ ...producerDetails })
|
||||
transport?.on('connect', async ({ dtlsParameters }, callback, errback) => {
|
||||
// 6. connectProducerTransport
|
||||
setStates(s => [...s, 'createSendTransport'])
|
||||
socket.current?.send(JSON.stringify({
|
||||
type: 'connectProducerTransport',
|
||||
dtlsParameters
|
||||
}))
|
||||
|
||||
socket.current?.addEventListener('message', ev => {
|
||||
// 7. producerTransportConnected
|
||||
// console.log(`SEOM TO CHK `, ev.data)
|
||||
if (JSON.parse(ev.data).type === 'producerTransportConnected') {
|
||||
setStates(s => [...s, 'producerTransportConnected'])
|
||||
// console.log(`------->producerTransportConnected:onCB`)
|
||||
callback()
|
||||
}
|
||||
})
|
||||
})
|
||||
transport?.on('produce', async ({ kind, rtpParameters }, callback, errback) => {
|
||||
// 8. Produce
|
||||
setStates(s => [...s, 'produce'])
|
||||
socket.current?.send(JSON.stringify({
|
||||
type: 'produce',
|
||||
transportId: transport.id,
|
||||
kind,
|
||||
rtpParameters
|
||||
}))
|
||||
socket.current?.addEventListener('message', ev => {
|
||||
if (ev.data.type === 'published') {
|
||||
callback(ev.data.id``)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
transport?.on('connectionstatechange', async (state) => {
|
||||
console.log(`connectionstatechange`, state)
|
||||
switch (state) {
|
||||
case 'connecting': break;
|
||||
case 'closed': break;
|
||||
case 'connected':
|
||||
// Link stream here
|
||||
break;
|
||||
case 'disconnected': break;
|
||||
case 'failed':
|
||||
transport.close()
|
||||
break;
|
||||
case 'new': break;
|
||||
}
|
||||
})
|
||||
transport?.on('icecandidateerror', async (error) => {
|
||||
console.log('icecandidateerror', error)
|
||||
})
|
||||
transport?.on('icegatheringstatechange', async (error) => {
|
||||
console.log('icegatheringstatechange', error)
|
||||
})
|
||||
transport?.on('producedata', async (error) => {
|
||||
console.log('producedata', error)
|
||||
})
|
||||
|
||||
let streamMedia
|
||||
try {
|
||||
streamMedia = await getUserMedia(transport, true) // is webcam
|
||||
const track = streamMedia!.getVideoTracks()[0]
|
||||
// const params - { track }
|
||||
// videoRef.current?.h = track
|
||||
console.log(`LocalStream`, streamMedia)
|
||||
// videoRef.current!.srcObject = streamMedia
|
||||
|
||||
const producer = await transport?.produce({ track })
|
||||
producer!.on("trackended", () => {
|
||||
console.log("track ended");
|
||||
});
|
||||
producer!.on("transportclose", () => {
|
||||
console.log("transport ended");
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`ERROR`, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const onSubTransportCreated = (consumerDetails: any) => {
|
||||
consumerTransport.current = device.current!.createRecvTransport({ ...consumerDetails })
|
||||
|
||||
console.log(`onSubTransportCreated`, consumerDetails, consumerTransport.current.connectionState)
|
||||
consumerTransport.current.on('connect', ({ dtlsParameters }, callback, errback) => {
|
||||
//11 . Accept connect
|
||||
|
||||
setStates(s => [...s, 'connectConsumerTransport'])
|
||||
socket.current?.send(JSON.stringify({
|
||||
type: 'connectConsumerTransport',
|
||||
transportId: consumerTransport.current!.id,
|
||||
dtlsParameters
|
||||
}))
|
||||
socket.current?.addEventListener('message', ev => {
|
||||
if (JSON.parse(ev.data).type === 'subConnected') {
|
||||
console.log(`subConnected**`, JSON.parse(ev.data).type)
|
||||
callback()
|
||||
}
|
||||
})
|
||||
})
|
||||
consumerTransport.current?.on('connectionstatechange', async (state) => {
|
||||
console.log(`connectionstatechange:`, state)
|
||||
switch (state) {
|
||||
case 'connecting':
|
||||
|
||||
break;
|
||||
case 'failed': console.log(`FILAED`); break;
|
||||
case 'connected':
|
||||
console.log(`remoteStream`, remoteStream)
|
||||
// videoBRef.current!.srcObject = remoteStream
|
||||
|
||||
socket.current?.send(JSON.stringify({
|
||||
type: 'resume',
|
||||
}))
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
})
|
||||
consumerTransport.current?.on('icecandidateerror', () => {
|
||||
console.log(`icecandidateerror`)
|
||||
})
|
||||
consumerTransport.current?.on('icegatheringstatechange', async (state) => {
|
||||
console.log(`icegatheringstatechange`, state)
|
||||
})
|
||||
|
||||
const stream = consumer(consumerTransport.current)
|
||||
}
|
||||
|
||||
const onSubscribe = async (details: any) => {
|
||||
console.log('onSubscribe', details)
|
||||
const {
|
||||
producerId,
|
||||
id,
|
||||
kind,
|
||||
rtpParameters,
|
||||
type,
|
||||
producerPaused,
|
||||
} = details
|
||||
|
||||
const codecOptions = {}
|
||||
const consumer = await consumerTransport.current!.consume({
|
||||
producerId,
|
||||
id,
|
||||
kind,
|
||||
rtpParameters,
|
||||
// type,
|
||||
// producerPaused,
|
||||
// codecOptions
|
||||
})
|
||||
const stream = new MediaStream()
|
||||
stream.addTrack(consumer.track)
|
||||
console.log(`TRYIN STREAM 1`,stream)
|
||||
videoBRef.current!.srcObject = stream
|
||||
}
|
||||
|
||||
const consumer = async (transport: any) => {
|
||||
const rtpCapabilities = device.current?.recvRtpCapabilities
|
||||
socket.current?.send(JSON.stringify({
|
||||
type: 'consume',
|
||||
rtpCapabilities
|
||||
}))
|
||||
}
|
||||
// const connectSendTransport = async () => {
|
||||
// const producer = await transport.produce(params);
|
||||
// console.log("Producer created:", producer.id, producer.kind);
|
||||
// producer.on("trackended", () => {
|
||||
// console.log("track ended");
|
||||
// });
|
||||
// producer.on("transportclose", () => {
|
||||
// console.log("transport ended");
|
||||
// });
|
||||
// };
|
||||
|
||||
const stream = () => {
|
||||
// 4.1 Start creating producers steam
|
||||
|
||||
// 4. Producers stream
|
||||
setStates(s => [...s, 'createProducerTransport'])
|
||||
socket.current?.send(JSON.stringify({
|
||||
type: 'createProducerTransport',
|
||||
forceTcp: false,
|
||||
rtpCapabilities: device.current?.sendRtpCapabilities
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
const joinStream = () => {
|
||||
setStates(s => [...s, 'createConsumerTransport'])
|
||||
socket.current?.send(JSON.stringify({
|
||||
type: 'createConsumerTransport',
|
||||
forceTcp: false,
|
||||
// rtpCapabilities: device.current?.sendRtpCapabilities
|
||||
}))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const parseWSMessage = (ev: any) => {
|
||||
const recv = JSON.parse(ev)
|
||||
// console.log(`-- parseWSMessage --`, recv.data)
|
||||
switch (recv.type) {
|
||||
case 'routerCapabilities':
|
||||
// 3. Received capabilities
|
||||
setStates(s => [...s, 'routerCapabilities'])
|
||||
loadDevices(recv.data);
|
||||
break;
|
||||
case 'producerTransportCreated':
|
||||
// 4.2 Received capabilities
|
||||
setStates(s => [...s, 'producerTransportCreated'])
|
||||
onProducerTransportCreated(recv.data);
|
||||
break;
|
||||
// case 'producerTransportConnected':
|
||||
// // 7. producerTransportConnected but not with callback
|
||||
// setStates(s => [...s, 'producerTransportCreated'])
|
||||
// // callback()
|
||||
// break;
|
||||
case 'newProducer':
|
||||
// 9 Found new Produce contents and send to all clients
|
||||
setStates(s => [...s, 'newProducer'])
|
||||
break;
|
||||
case 'subTransportCreated':
|
||||
// 10 Consumer joined
|
||||
setStates(s => [...s, 'subTransportCreated'])
|
||||
onSubTransportCreated(recv.data);
|
||||
break;
|
||||
case 'resumed':
|
||||
// 10 Consumer joined
|
||||
setStates(s => [...s, 'resumed'])
|
||||
console.log(`resumed`, recv.data)
|
||||
break;
|
||||
case 'subscribed':
|
||||
// 12 Consumer joined
|
||||
setStates(s => [...s, 'subscribed'])
|
||||
onSubscribe(recv.data)
|
||||
break;
|
||||
default:
|
||||
console.log(`Received Uknown`, recv.type)
|
||||
|
||||
break;
|
||||
}
|
||||
// console.log(ev)
|
||||
|
||||
}
|
||||
return (
|
||||
<div>WebRTCChat
|
||||
<div>
|
||||
<div className={`${states.includes('ws-connected') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Connected To Server</div>
|
||||
<small>Establish connection to the WS Server</small>
|
||||
</div>
|
||||
<div className={`${states.includes('getRouterRtpCapabilities') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Checking Server Capabilities</div>
|
||||
<small>getRouterRtpCapabilities</small>
|
||||
</div>
|
||||
<div className={`${states.includes('routerCapabilities') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Has Server Capabilities</div>
|
||||
<small>routerCapabilities</small>
|
||||
</div>
|
||||
<div className={`${states.includes('createProducerTransport') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Create Producers Transport (Waiting for user to start stream)</div>
|
||||
<small>createProducerTransport</small>
|
||||
</div>
|
||||
<div className={`${states.includes('producerTransportCreated') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Created Producers Transport</div>
|
||||
<small>producerTransportCreated</small>
|
||||
</div>
|
||||
<div className={`${states.includes('producerTransportCreated') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Created Producers Transport</div>
|
||||
<small>producerTransportCreated</small>
|
||||
</div>
|
||||
<div className={`${states.includes('createSendTransport') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Created Send Transport</div>
|
||||
<small>createSendTransport</small>
|
||||
</div>
|
||||
<div className={`${states.includes('producerTransportConnected') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Producer Transport Connected</div>
|
||||
<small>producerTransportConnected</small>
|
||||
</div>
|
||||
<div className={`${states.includes('producerTransportConnected') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Send Produced</div>
|
||||
<small>produce</small>
|
||||
</div>
|
||||
<div className={`${states.includes('newProducer') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Found new Produce contents and send to all clients</div>
|
||||
<small>newProducer</small>
|
||||
</div>
|
||||
<div className={`${states.includes('subTransportCreated') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Cunsumber connected</div>
|
||||
<small>subTransportCreated</small>
|
||||
</div>
|
||||
<div className={`${states.includes('connectConsumerTransport') ? 'text-green-400' : 'text-gray-400'} border`}>
|
||||
<div>Accept Consumer Connection</div>
|
||||
<small>connectConsumerTransport</small>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
socket.current = new WebSocket(wsURL)
|
||||
socket.current.onopen = () => {
|
||||
// 1. Create websocket connection
|
||||
setStates(s => [...s, 'ws-connected'])
|
||||
const msg = {
|
||||
type: "getRouterRtpCapabilities"
|
||||
}
|
||||
try {
|
||||
// 2. Get Server capabilities
|
||||
setStates(s => [...s, 'getRouterRtpCapabilities'])
|
||||
socket.current?.send(JSON.stringify(msg))
|
||||
} catch (e) {
|
||||
console.log(`Failed to send capabilities`, e)
|
||||
}
|
||||
}
|
||||
socket.current.onmessage = e => parseWSMessage((e as any).data)
|
||||
}}
|
||||
disabled={connected}
|
||||
>{connected ? 'Connected' : 'Connect'}</button>
|
||||
<button onClick={() => stream()}>
|
||||
Stream
|
||||
</button>
|
||||
<video ref={videoRef}></video>
|
||||
<button
|
||||
onClick={() => {
|
||||
joinStream()
|
||||
}}
|
||||
|
||||
>Join</button>
|
||||
<video ref={videoBRef}></video>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
187
webapp/components/chat/WebRTCChatUsingPeer copy 2.tmp
Normal file
@ -0,0 +1,187 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import { CameraIcon, HdIcon, MicrochipIcon, Server, VideoIcon } from 'lucide-react';
|
||||
import Peer, { DataConnection } from 'peerjs';
|
||||
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
type FlowStateType = {
|
||||
color: 'red' | 'blue' | 'green' | 'orange',
|
||||
flow: 'MIC' | 'VIDEO' | 'SERVER' | 'NO_PEERS' | 'ROOM',
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export default function WebRTCChatUsingPeer() {
|
||||
let mediaStream
|
||||
// UI
|
||||
|
||||
|
||||
const peerRef = useRef<Peer>(undefined)
|
||||
const [peers, setPeers] = useState<string[]>([])
|
||||
|
||||
|
||||
const connection = useRef<DataConnection>(undefined)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
// const [mediaStream, setMediaStream] = useState<MediaStream>(null)
|
||||
const [received, setReceivved] = useState<string>('')
|
||||
const [peerId, setPeerId] = useState<string>('')
|
||||
const [theirPeerId, setTheirPeerId] = useState<string>('')
|
||||
const [myStream, setMyStream] = useState<MediaStream>(null);
|
||||
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
const [selectedDevice, setSelectedDevice] = useState<{ video: { deviceId: { exact: string } }, audio: { deviceId: { exact: string } } }>()
|
||||
|
||||
const peer = new Peer({
|
||||
host: "localhost",
|
||||
port: 4000,
|
||||
path: "/myapp",
|
||||
secure: false, // Use true with HTTPS
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
peer.on("open", function (id) {
|
||||
setPeerId(id)
|
||||
console.log("My peer ID is: " + id);
|
||||
});
|
||||
}, [])
|
||||
|
||||
|
||||
peer.on('connection', function (conn) {
|
||||
console.log(`Got Connection`, conn)
|
||||
conn.on("data", function (data) {
|
||||
setReceivved(data as string)
|
||||
console.log("Received", data);
|
||||
});
|
||||
conn.send("Hello!");
|
||||
});
|
||||
// useEffect(() => {
|
||||
// // Get user media
|
||||
// navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// .then(stream => {
|
||||
// // setMyStream(stream);
|
||||
// // if (videoRef.current) {
|
||||
// // videoRef.current.srcObject = stream;
|
||||
// // }
|
||||
// })
|
||||
// .catch(err => console.error("Failed to get local stream", err));
|
||||
|
||||
// // Initialize PeerJS
|
||||
// // const peer = new Peer(); // PeerJS manages server connection
|
||||
// // currentPeer.current = peer;
|
||||
|
||||
// peer.on('open', (id) => {
|
||||
// setPeerId(id);
|
||||
// console.log('My peer ID is:', id);
|
||||
// });
|
||||
|
||||
// // Listen for incoming calls
|
||||
// peer.on('call', (call) => {
|
||||
// if (myStream) {
|
||||
// // Answer the call with your stream
|
||||
// call.answer(myStream);
|
||||
// call.on('stream', (remoteStream) => {
|
||||
// // Show stream in remote video element
|
||||
// if (videoRef.current) {
|
||||
// videoRef.current.srcObject = remoteStream;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// peer.on('error', (err) => console.error(err));
|
||||
|
||||
// return () => {
|
||||
// if (peer) {
|
||||
// peer.destroy();
|
||||
// }
|
||||
// };
|
||||
// }, [myStream]);
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// const load = async () => {
|
||||
// // Load media devices
|
||||
// const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
// setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
// // )
|
||||
// // setFlowIdx(1)
|
||||
// // setFlowIdx(2)
|
||||
|
||||
// // console.log(await navigator.mediaDevices.enumerateDevices())
|
||||
// // console.log(await navigator.mediaDevices.getUserMedia().)
|
||||
// }
|
||||
// load()
|
||||
// peer.on("call", function (call) {
|
||||
// alert(`You have a call`)
|
||||
// // Answer the call, providing our mediaStream
|
||||
// // if (mediaStream) {
|
||||
// call.answer(mediaStream);
|
||||
|
||||
// call.on('stream', (remoteStream) => {
|
||||
// // Show stream in remote video element
|
||||
// if (videoRef.current) {
|
||||
// videoRef.current.srcObject = remoteStream;
|
||||
// }
|
||||
// });
|
||||
// // }
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{JSON.stringify(peerId)}
|
||||
<div>
|
||||
<b>Settings</b>
|
||||
<b>Video</b>
|
||||
{devices.filter(a => a.kind === 'videoinput').map(device =>
|
||||
<div key={device.deviceId}
|
||||
onClick={() => setSelectedDevice((d: any) => {
|
||||
return { ...d, video: { deviceId: { exact: device.deviceId } } }
|
||||
})}
|
||||
className={`${selectedDevice && selectedDevice.video && device.deviceId === selectedDevice?.video.deviceId.exact ? 'bg-green-300' : ''}`}
|
||||
>
|
||||
{device.label}
|
||||
</div>
|
||||
)}
|
||||
<b>Audio</b>
|
||||
{devices.filter(a => a.kind === 'audioinput').map(device =>
|
||||
<div key={device.deviceId}
|
||||
onClick={() => setSelectedDevice((d: any) => {
|
||||
return { ...d, audio: { deviceId: { exact: device.deviceId } } }
|
||||
})}
|
||||
className={`${selectedDevice && selectedDevice.audio && device.deviceId === selectedDevice?.audio.deviceId.exact ? 'bg-green-300' : ''}`}
|
||||
|
||||
>
|
||||
{device.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input placeholder='Cannect to' onChange={e => setTheirPeerId(e.target.value)} />
|
||||
<button onClick={() => {
|
||||
console.log(`Connecting`)
|
||||
connection.current
|
||||
= peer.connect(theirPeerId, {
|
||||
|
||||
});
|
||||
}}>Start</button>
|
||||
<button
|
||||
className='p-2'
|
||||
onClick={() => {
|
||||
// const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
|
||||
navigator.mediaDevices.getUserMedia(selectedDevice)
|
||||
.then(stream => peer.call(theirPeerId, stream))
|
||||
// const call = peer.call(theirPeerId, stream);
|
||||
|
||||
// const stream = await navigator.mediaDevices.getUserMedia({
|
||||
// video: { deviceId: { exact: selectedVideoId } }
|
||||
// });
|
||||
}}
|
||||
>Call</button>
|
||||
<textarea onChange={e => connection.current?.send(e.target.value)} placeholder='Message' rows={6} />
|
||||
{JSON.stringify(received)}
|
||||
<video ref={videoRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
530
webapp/components/chat/WebRTCChatUsingPeer copy 3.tmp
Normal file
@ -0,0 +1,530 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import { CameraIcon, HdIcon, MicrochipIcon, Server, VideoIcon } from 'lucide-react';
|
||||
import Peer, { DataConnection } from 'peerjs';
|
||||
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
type FlowStateType = {
|
||||
color: 'red' | 'blue' | 'green' | 'orange',
|
||||
flow: 'MIC' | 'VIDEO' | 'SERVER' | 'NO_PEERS' | 'ROOM',
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export default function WebRTCChatUsingPeer() {
|
||||
let mediaStream
|
||||
// UI
|
||||
|
||||
// const [flowSection, setFlowSection] = useState<'MIC' | 'VIDEO' | 'SERVER'>('MIC')
|
||||
const [flowState, setFlowState] = useState<FlowStateType>({
|
||||
color: 'blue',
|
||||
flow: 'SERVER',
|
||||
loading: false
|
||||
})
|
||||
|
||||
|
||||
// const [flow, setFlow] = useState<{
|
||||
// screen: ReactNode,
|
||||
// loading: boolean,
|
||||
// color: 'red' | 'blue' | 'green' | 'orange',
|
||||
// options?: any
|
||||
// }[]>([
|
||||
// // {
|
||||
// // loading: false,
|
||||
// // screen: <SelectMicrophone setLoading={setLoading} />,
|
||||
// // color: 'orange',
|
||||
// // options: [{
|
||||
// // label: 'Next',
|
||||
// // onSelect: (a: any) => { }
|
||||
// // }]
|
||||
// // },
|
||||
// {
|
||||
// loading: false,
|
||||
// screen: <>
|
||||
// <CameraIcon size={45} />
|
||||
// Select video device
|
||||
// <select>
|
||||
// <option>Select</option>
|
||||
// </select>
|
||||
// </>,
|
||||
// color: 'orange'
|
||||
// }, {
|
||||
// loading: true,
|
||||
// screen: <ConnectingToSerever />,
|
||||
// color: 'blue'
|
||||
// }])
|
||||
|
||||
|
||||
const peerRef = useRef<Peer>(undefined)
|
||||
const [peers, setPeers] = useState<string[]>([])
|
||||
|
||||
|
||||
const connection = useRef<DataConnection>(undefined)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
// const [mediaStream, setMediaStream] = useState<MediaStream>(null)
|
||||
const [received, setReceivved] = useState<string>('')
|
||||
const [peerId, setPeerId] = useState<string>('')
|
||||
const [theirPeerId, setTheirPeerId] = useState<string>('')
|
||||
const [myStream, setMyStream] = useState<MediaStream>(null);
|
||||
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
const [selectedDevice, setSelectedDevice] = useState<{ video: { deviceId: { exact: string } }, audio: { deviceId: { exact: string } } }>()
|
||||
|
||||
const handleFindPeers = () => {
|
||||
peerRef.current?.listAllPeers(peers => {
|
||||
setPeers(() => peers)
|
||||
console.log(`PEERS`, peers)
|
||||
if (peers.length > 1) {
|
||||
console.log(`CONNECT TO: `, peers.filter(a => a !== peerId)[0])
|
||||
if (!connection.current)
|
||||
peerRef.current?.connect(peers.filter(a => a !== peerId)[0])
|
||||
setFlowState(s => { return { ...s, flow: 'ROOM', color: 'blue', loading: true } })
|
||||
} else {
|
||||
setFlowState(s => { return { ...s, flow: 'NO_PEERS', color: 'blue', loading: true } })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleConnectServer = () => {
|
||||
setFlowState(s => { return { ...s, flow: 'SERVER' } })
|
||||
|
||||
peerRef.current = new Peer({
|
||||
host: "localhost",
|
||||
port: 4000,
|
||||
path: "/myapp",
|
||||
secure: false, // Use true with HTTPS
|
||||
})
|
||||
|
||||
// peerRef.current.socket.on('message')
|
||||
peerRef.current.on("open", function (id) {
|
||||
setPeerId(id)
|
||||
setFlowState(s => { return { ...s, color: 'green', loading: false } })
|
||||
console.log("My peer ID is: " + id, 'Connected Peers:',);
|
||||
handleFindPeers()
|
||||
});
|
||||
|
||||
peerRef.current.on('error', function (err) {
|
||||
setFlowState(s => { return { ...s, color: 'red', loading: false } })
|
||||
})
|
||||
|
||||
peerRef.current.on('connection', function (conn) {
|
||||
console.log(`Got Connection`, conn)
|
||||
connection.current = conn
|
||||
// handleFindPeers()
|
||||
setFlowState(s => { return { ...s, flow: 'ROOM' } })
|
||||
connection.current.on("data", function (data) {
|
||||
// setReceivved(data as string)
|
||||
console.log("Received", data);
|
||||
});
|
||||
connection.current.send("Hello!");
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleConnectServer()
|
||||
}, [])
|
||||
|
||||
|
||||
// const peer = new Peer({
|
||||
// host: "localhost",
|
||||
// port: 4000,
|
||||
// path: "/myapp",
|
||||
// secure: false, // Use true with HTTPS
|
||||
// });
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// peer.on("open", function (id) {
|
||||
// setPeerId(id)
|
||||
// console.log("My peer ID is: " + id);
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
|
||||
// peer.on('connection', function (conn) {
|
||||
// console.log(`Got Connection`, conn)
|
||||
// conn.on("data", function (data) {
|
||||
// setReceivved(data as string)
|
||||
// console.log("Received", data);
|
||||
// });
|
||||
// conn.send("Hello!");
|
||||
// });
|
||||
// useEffect(() => {
|
||||
// // Get user media
|
||||
// navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// .then(stream => {
|
||||
// // setMyStream(stream);
|
||||
// // if (videoRef.current) {
|
||||
// // videoRef.current.srcObject = stream;
|
||||
// // }
|
||||
// })
|
||||
// .catch(err => console.error("Failed to get local stream", err));
|
||||
|
||||
// // Initialize PeerJS
|
||||
// // const peer = new Peer(); // PeerJS manages server connection
|
||||
// // currentPeer.current = peer;
|
||||
|
||||
// peer.on('open', (id) => {
|
||||
// setPeerId(id);
|
||||
// console.log('My peer ID is:', id);
|
||||
// });
|
||||
|
||||
// // Listen for incoming calls
|
||||
// peer.on('call', (call) => {
|
||||
// if (myStream) {
|
||||
// // Answer the call with your stream
|
||||
// call.answer(myStream);
|
||||
// call.on('stream', (remoteStream) => {
|
||||
// // Show stream in remote video element
|
||||
// if (videoRef.current) {
|
||||
// videoRef.current.srcObject = remoteStream;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// peer.on('error', (err) => console.error(err));
|
||||
|
||||
// return () => {
|
||||
// if (peer) {
|
||||
// peer.destroy();
|
||||
// }
|
||||
// };
|
||||
// }, [myStream]);
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// const load = async () => {
|
||||
// // Load media devices
|
||||
// const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
// setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
// // )
|
||||
// // setFlowIdx(1)
|
||||
// // setFlowIdx(2)
|
||||
|
||||
// // console.log(await navigator.mediaDevices.enumerateDevices())
|
||||
// // console.log(await navigator.mediaDevices.getUserMedia().)
|
||||
// }
|
||||
// load()
|
||||
// peer.on("call", function (call) {
|
||||
// alert(`You have a call`)
|
||||
// // Answer the call, providing our mediaStream
|
||||
// // if (mediaStream) {
|
||||
// call.answer(mediaStream);
|
||||
|
||||
// call.on('stream', (remoteStream) => {
|
||||
// // Show stream in remote video element
|
||||
// if (videoRef.current) {
|
||||
// videoRef.current.srcObject = remoteStream;
|
||||
// }
|
||||
// });
|
||||
// // }
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
if (flowState.flow === 'ROOM') {
|
||||
return <div className='flex w-screen h-screen'>
|
||||
<div className='grow'></div>
|
||||
<form onSubmit={(form) => {
|
||||
form.preventDefault()
|
||||
const formData = new FormData(form.target);
|
||||
// const det = Object.fromEntries(formData.entries())
|
||||
try {
|
||||
console.log(`Sending:`, connection.current, formData.get('message') as string)
|
||||
connection.current?.send(formData.get('message') as string)
|
||||
} catch (e) {
|
||||
console.error('Failed to send message', e)
|
||||
}
|
||||
}}>
|
||||
<input name='message' placeholder='message' />
|
||||
<button type='submit'>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
return (<Screen
|
||||
color={flowState.color}
|
||||
loading={flowState.loading}>
|
||||
{
|
||||
{
|
||||
'MIC': <SelectMicrophone setFlowState={setFlowState}
|
||||
onChange={e => {
|
||||
setSelectedDevice((d: any) => {
|
||||
return { ...d, audio: { deviceId: { exact: e } } }
|
||||
})
|
||||
setFlowState(s => { return { ...s, flow: 'VIDEO' } })
|
||||
// setFlowSection('VIDEO')
|
||||
}}
|
||||
/>,
|
||||
'VIDEO': <SelectVideo setFlowState={setFlowState}
|
||||
onChange={e => {
|
||||
setSelectedDevice((d: any) => {
|
||||
return { ...d, video: { deviceId: { exact: e } } }
|
||||
})
|
||||
handleConnectServer()
|
||||
}}
|
||||
/>,
|
||||
'SERVER': <ConnectingToSerever
|
||||
setFlowState={setFlowState}
|
||||
/>,
|
||||
'NO_PEERS': <>Waiting for others to join</>,
|
||||
'ROOM': <></>
|
||||
}[flowState.flow]
|
||||
}
|
||||
{/* {flow[flowIdx].screen} */}
|
||||
</Screen>)
|
||||
// return (
|
||||
// <div>
|
||||
// {JSON.stringify(peerId)}
|
||||
// <div>
|
||||
// <b>Settings</b>
|
||||
// <b>Video</b>
|
||||
// {devices.filter(a => a.kind === 'videoinput').map(device =>
|
||||
// <div key={device.deviceId}
|
||||
// onClick={() => setSelectedDevice((d: any) => {
|
||||
// return { ...d, video: { deviceId: { exact: device.deviceId } } }
|
||||
// })}
|
||||
// className={`${selectedDevice && selectedDevice.video && device.deviceId === selectedDevice?.video.deviceId.exact ? 'bg-green-300' : ''}`}
|
||||
// >
|
||||
// {device.label}
|
||||
// </div>
|
||||
// )}
|
||||
// <b>Audio</b>
|
||||
// {devices.filter(a => a.kind === 'audioinput').map(device =>
|
||||
// <div key={device.deviceId}
|
||||
// onClick={() => setSelectedDevice((d: any) => {
|
||||
// return { ...d, audio: { deviceId: { exact: device.deviceId } } }
|
||||
// })}
|
||||
// className={`${selectedDevice && selectedDevice.audio && device.deviceId === selectedDevice?.audio.deviceId.exact ? 'bg-green-300' : ''}`}
|
||||
|
||||
// >
|
||||
// {device.label}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// <input placeholder='Cannect to' onChange={e => setTheirPeerId(e.target.value)} />
|
||||
// <button onClick={() => {
|
||||
// console.log(`Connecting`)
|
||||
// connection.current
|
||||
// = peer.connect(theirPeerId, {
|
||||
|
||||
// });
|
||||
// }}>Start</button>
|
||||
// <button
|
||||
// className='p-2'
|
||||
// onClick={() => {
|
||||
// // const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
|
||||
// navigator.mediaDevices.getUserMedia(selectedDevice)
|
||||
// .then(stream => peer.call(theirPeerId, stream))
|
||||
// // const call = peer.call(theirPeerId, stream);
|
||||
|
||||
// // const stream = await navigator.mediaDevices.getUserMedia({
|
||||
// // video: { deviceId: { exact: selectedVideoId } }
|
||||
// // });
|
||||
// }}
|
||||
// >Call</button>
|
||||
// <textarea onChange={e => connection.current?.send(e.target.value)} placeholder='Message' rows={6} />
|
||||
// {JSON.stringify(received)}
|
||||
// <video ref={videoRef} />
|
||||
// </div>
|
||||
// )
|
||||
}
|
||||
|
||||
|
||||
export function Screen({ color, loading, children }: {
|
||||
color: 'red' | 'blue' | 'green' | 'orange',
|
||||
loading: boolean
|
||||
children: ReactNode
|
||||
}) {
|
||||
return <div
|
||||
style={{
|
||||
background: color == 'orange' ? '#441306' : color == 'green' ? '#032e15' : color == 'red' ? '#460809' : '#162456'
|
||||
}}
|
||||
className={`h-screen w-screen flex items-center align-middle justify-center`}>
|
||||
<>
|
||||
<div className='relative '>
|
||||
{/* <div className='absolute flex items-center align-middle justify-center -left-20 -top-10 bg-blue-900 hover:bg-blue-800 shadow shadow-white shadow-2xl rounded-full grow w-64 h-64'>
|
||||
ITEM 1
|
||||
</div>
|
||||
<div className='absolute right-0 bg-blue-950 hover:bg-blue-900 w-64 h-64'>
|
||||
ITEM 2
|
||||
</div>
|
||||
<div className='absolute bottom-0 bg-blue-950 hover:bg-blue-900'>
|
||||
ITEM 3
|
||||
</div> */}
|
||||
<div
|
||||
style={{
|
||||
background: color == 'orange' ?
|
||||
'radial-gradient(circle, oklch(40.8% 0.123 38.172) 0%, rgba(0,0,0,0.3) 100%)'
|
||||
: color == 'green' ?
|
||||
'radial-gradient(circle, oklch(39.3% 0.095 152.535) 0%, rgba(0,0,0,0.3) 100%)' :
|
||||
color == 'red' ?
|
||||
'radial-gradient(circle,oklch(39.6% 0.141 25.723) 0%, rgba(0,0,0,0.3) 100%)'
|
||||
:
|
||||
'radial-gradient(circle,oklch(37.9% 0.146 265.522) 0%, rgba(0,0,0,0.3) 100%)'
|
||||
|
||||
}}
|
||||
className={`w-96 h-95 rounded-full relative border-8 shadow-lg ${loading && `animate-spin shadow-purple-300/75`} text-3xl text-white flex items-center justify-center align-middle `}>
|
||||
<div className="absolute inset-0 flex items-center justify-center animate-[inherit] direction-reverse">
|
||||
<span className="flex gap-2 flex-col items-center font-bold text-white text-shadow-white text-shadow-2xs">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function ConnectingToSerever({
|
||||
setFlowState,
|
||||
}: {
|
||||
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
|
||||
}) {
|
||||
useEffect(() => { setFlowState(s => { return { ...s, color: 'blue', loading: true } }) }, [])
|
||||
|
||||
|
||||
// const peerConn = useCallback(() => {
|
||||
|
||||
// const peer = new Peer({
|
||||
// host: "localhost",
|
||||
// port: 4000,
|
||||
// path: "/myapp",
|
||||
// secure: false, // Use true with HTTPS
|
||||
// });
|
||||
// peer.on("open", function (id) {
|
||||
// setPeerId(id)
|
||||
// setColor('green')
|
||||
// console.log("My peer ID is: " + id);
|
||||
// });
|
||||
|
||||
// peer.on('error', function (err) {
|
||||
// setColor('red')
|
||||
// setLoading(false)
|
||||
// })
|
||||
|
||||
// peer.on('connection', function (conn) {
|
||||
// console.log(`Got Connection`, conn)
|
||||
// conn.on("data", function (data) {
|
||||
// // setReceivved(data as string)
|
||||
// console.log("Received", data);
|
||||
// });
|
||||
// conn.send("Hello!");
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
// useEffect(() => {
|
||||
// const load = async () => {
|
||||
// peer.on("open", function (id) {
|
||||
// setPeerId(id)
|
||||
// setColor('green')
|
||||
// console.log("My peer ID is: " + id);
|
||||
// });
|
||||
|
||||
// peer.on('error', function (err) {
|
||||
// setColor('red')
|
||||
// setLoading(false)
|
||||
// })
|
||||
|
||||
// peer.on('connection', function (conn) {
|
||||
// console.log(`Got Connection`, conn)
|
||||
// conn.on("data", function (data) {
|
||||
// // setReceivved(data as string)
|
||||
// console.log("Received", data);
|
||||
// });
|
||||
// conn.send("Hello!");
|
||||
// });
|
||||
|
||||
// }
|
||||
// setColor('green')
|
||||
// setLoading(true)
|
||||
// load()
|
||||
// }, [])
|
||||
return <>
|
||||
<Server size={45} />
|
||||
Connecting to Server
|
||||
</>
|
||||
}
|
||||
|
||||
export function SelectMicrophone({
|
||||
setFlowState,
|
||||
onChange
|
||||
}: {
|
||||
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
|
||||
onChange: (device: string) => void
|
||||
}) {
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
// Load media devices
|
||||
// setTimeout(() => setLoading(false), 3000)
|
||||
// setLoading(false)
|
||||
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
|
||||
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
|
||||
}
|
||||
setFlowState(s => { return { ...s, color: 'green', loading: true } })
|
||||
load()
|
||||
}, [])
|
||||
return <>
|
||||
<MicrochipIcon size={45} />
|
||||
{
|
||||
devices.length == 0 ? 'Finding Audio Devices' : <>
|
||||
|
||||
Select audio device
|
||||
<select className='text-sm w-64 bg-white p-2 text-black'
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
<option>Select</option>
|
||||
{devices.filter(a => a.kind === 'audioinput').map((device, i) =>
|
||||
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
|
||||
)}
|
||||
|
||||
</select>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export function SelectVideo({
|
||||
setFlowState,
|
||||
onChange
|
||||
}: {
|
||||
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
|
||||
onChange: (device: string) => void
|
||||
}) {
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
// Load media devices
|
||||
// setTimeout(() => setLoading(false), 3000)
|
||||
// setLoading(false)
|
||||
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
|
||||
}
|
||||
setFlowState(s => { return { ...s, color: 'green', loading: true } })
|
||||
load()
|
||||
}, [])
|
||||
return <>
|
||||
<VideoIcon size={45} />
|
||||
{
|
||||
devices.length == 0 ? 'Finding Video Devices' : <>
|
||||
|
||||
Select video device
|
||||
<select className='text-sm w-64 bg-white p-2 text-black'
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
<option>Select</option>
|
||||
{devices.filter(a => a.kind === 'videoinput').map((device, i) =>
|
||||
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
|
||||
)}
|
||||
|
||||
</select>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
506
webapp/components/chat/WebRTCChatUsingPeer copy.tmp
Normal file
@ -0,0 +1,506 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import { CameraIcon, HdIcon, MicrochipIcon, Server, VideoIcon } from 'lucide-react';
|
||||
import Peer, { DataConnection } from 'peerjs';
|
||||
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
type FlowStateType = {
|
||||
color: 'red' | 'blue' | 'green' | 'orange',
|
||||
flow: 'MIC' | 'VIDEO' | 'SERVER' | 'NO_PEERS' | 'ROOM',
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export default function WebRTCChatUsingPeer() {
|
||||
let mediaStream
|
||||
// UI
|
||||
|
||||
// const [flowSection, setFlowSection] = useState<'MIC' | 'VIDEO' | 'SERVER'>('MIC')
|
||||
const [flowState, setFlowState] = useState<FlowStateType>({
|
||||
color: 'blue',
|
||||
flow: 'SERVER',
|
||||
loading: false
|
||||
})
|
||||
|
||||
|
||||
// const [flow, setFlow] = useState<{
|
||||
// screen: ReactNode,
|
||||
// loading: boolean,
|
||||
// color: 'red' | 'blue' | 'green' | 'orange',
|
||||
// options?: any
|
||||
// }[]>([
|
||||
// // {
|
||||
// // loading: false,
|
||||
// // screen: <SelectMicrophone setLoading={setLoading} />,
|
||||
// // color: 'orange',
|
||||
// // options: [{
|
||||
// // label: 'Next',
|
||||
// // onSelect: (a: any) => { }
|
||||
// // }]
|
||||
// // },
|
||||
// {
|
||||
// loading: false,
|
||||
// screen: <>
|
||||
// <CameraIcon size={45} />
|
||||
// Select video device
|
||||
// <select>
|
||||
// <option>Select</option>
|
||||
// </select>
|
||||
// </>,
|
||||
// color: 'orange'
|
||||
// }, {
|
||||
// loading: true,
|
||||
// screen: <ConnectingToSerever />,
|
||||
// color: 'blue'
|
||||
// }])
|
||||
|
||||
|
||||
const peerRef = useRef<Peer>(undefined)
|
||||
const [peers, setPeers] = useState<string[]>([])
|
||||
|
||||
|
||||
const connection = useRef<DataConnection>(undefined)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
// const [mediaStream, setMediaStream] = useState<MediaStream>(null)
|
||||
const [received, setReceivved] = useState<string>('')
|
||||
const [peerId, setPeerId] = useState<string>('')
|
||||
const [theirPeerId, setTheirPeerId] = useState<string>('')
|
||||
const [myStream, setMyStream] = useState<MediaStream>(null);
|
||||
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
const [selectedDevice, setSelectedDevice] = useState<{ video: { deviceId: { exact: string } }, audio: { deviceId: { exact: string } } }>()
|
||||
|
||||
const handleFindPeers = () => {
|
||||
peerRef.current?.listAllPeers(peers => {
|
||||
setPeers(() => peers)
|
||||
console.log(`PEERS`, peers)
|
||||
if (peers.length > 1) {
|
||||
peerRef.current?.connect(peers[1])
|
||||
setFlowState(s => { return { ...s, flow: 'ROOM', color: 'blue', loading: true } })
|
||||
} else {
|
||||
setFlowState(s => { return { ...s, flow: 'NO_PEERS', color: 'blue', loading: true } })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleConnectServer = () => {
|
||||
setFlowState(s => { return { ...s, flow: 'SERVER' } })
|
||||
|
||||
peerRef.current = new Peer({
|
||||
host: "localhost",
|
||||
port: 4000,
|
||||
path: "/myapp",
|
||||
secure: false, // Use true with HTTPS
|
||||
})
|
||||
|
||||
// peerRef.current.socket.on('message')
|
||||
peerRef.current.on("open", function (id) {
|
||||
setPeerId(id)
|
||||
setFlowState(s => { return { ...s, color: 'green', loading: false } })
|
||||
console.log("My peer ID is: " + id, 'Connected Peers:',);
|
||||
handleFindPeers()
|
||||
});
|
||||
|
||||
peerRef.current.on('error', function (err) {
|
||||
setFlowState(s => { return { ...s, color: 'red', loading: false } })
|
||||
})
|
||||
|
||||
peerRef.current.on('connection', function (conn) {
|
||||
console.log(`Got Connection`, conn)
|
||||
conn.on("data", function (data) {
|
||||
// setReceivved(data as string)
|
||||
console.log("Received", data);
|
||||
});
|
||||
conn.send("Hello!");
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleConnectServer()
|
||||
}, [])
|
||||
|
||||
|
||||
// const peer = new Peer({
|
||||
// host: "localhost",
|
||||
// port: 4000,
|
||||
// path: "/myapp",
|
||||
// secure: false, // Use true with HTTPS
|
||||
// });
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// peer.on("open", function (id) {
|
||||
// setPeerId(id)
|
||||
// console.log("My peer ID is: " + id);
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
|
||||
// peer.on('connection', function (conn) {
|
||||
// console.log(`Got Connection`, conn)
|
||||
// conn.on("data", function (data) {
|
||||
// setReceivved(data as string)
|
||||
// console.log("Received", data);
|
||||
// });
|
||||
// conn.send("Hello!");
|
||||
// });
|
||||
// useEffect(() => {
|
||||
// // Get user media
|
||||
// navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// .then(stream => {
|
||||
// // setMyStream(stream);
|
||||
// // if (videoRef.current) {
|
||||
// // videoRef.current.srcObject = stream;
|
||||
// // }
|
||||
// })
|
||||
// .catch(err => console.error("Failed to get local stream", err));
|
||||
|
||||
// // Initialize PeerJS
|
||||
// // const peer = new Peer(); // PeerJS manages server connection
|
||||
// // currentPeer.current = peer;
|
||||
|
||||
// peer.on('open', (id) => {
|
||||
// setPeerId(id);
|
||||
// console.log('My peer ID is:', id);
|
||||
// });
|
||||
|
||||
// // Listen for incoming calls
|
||||
// peer.on('call', (call) => {
|
||||
// if (myStream) {
|
||||
// // Answer the call with your stream
|
||||
// call.answer(myStream);
|
||||
// call.on('stream', (remoteStream) => {
|
||||
// // Show stream in remote video element
|
||||
// if (videoRef.current) {
|
||||
// videoRef.current.srcObject = remoteStream;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// peer.on('error', (err) => console.error(err));
|
||||
|
||||
// return () => {
|
||||
// if (peer) {
|
||||
// peer.destroy();
|
||||
// }
|
||||
// };
|
||||
// }, [myStream]);
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// const load = async () => {
|
||||
// // Load media devices
|
||||
// const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
// setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
// // )
|
||||
// // setFlowIdx(1)
|
||||
// // setFlowIdx(2)
|
||||
|
||||
// // console.log(await navigator.mediaDevices.enumerateDevices())
|
||||
// // console.log(await navigator.mediaDevices.getUserMedia().)
|
||||
// }
|
||||
// load()
|
||||
// peer.on("call", function (call) {
|
||||
// alert(`You have a call`)
|
||||
// // Answer the call, providing our mediaStream
|
||||
// // if (mediaStream) {
|
||||
// call.answer(mediaStream);
|
||||
|
||||
// call.on('stream', (remoteStream) => {
|
||||
// // Show stream in remote video element
|
||||
// if (videoRef.current) {
|
||||
// videoRef.current.srcObject = remoteStream;
|
||||
// }
|
||||
// });
|
||||
// // }
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
return (<Screen
|
||||
color={flowState.color}
|
||||
loading={flowState.loading}>
|
||||
{
|
||||
{
|
||||
'MIC': <SelectMicrophone setFlowState={setFlowState}
|
||||
onChange={e => {
|
||||
setSelectedDevice((d: any) => {
|
||||
return { ...d, audio: { deviceId: { exact: e } } }
|
||||
})
|
||||
setFlowState(s => { return { ...s, flow: 'VIDEO' } })
|
||||
// setFlowSection('VIDEO')
|
||||
}}
|
||||
/>,
|
||||
'VIDEO': <SelectVideo setFlowState={setFlowState}
|
||||
onChange={e => {
|
||||
setSelectedDevice((d: any) => {
|
||||
return { ...d, video: { deviceId: { exact: e } } }
|
||||
})
|
||||
handleConnectServer()
|
||||
}}
|
||||
/>,
|
||||
'SERVER': <ConnectingToSerever
|
||||
setFlowState={setFlowState}
|
||||
/>,
|
||||
'NO_PEERS': <>Waiting for others to join</>,
|
||||
'ROOM': <></>
|
||||
}[flowState.flow]
|
||||
}
|
||||
{/* {flow[flowIdx].screen} */}
|
||||
</Screen>)
|
||||
// return (
|
||||
// <div>
|
||||
// {JSON.stringify(peerId)}
|
||||
// <div>
|
||||
// <b>Settings</b>
|
||||
// <b>Video</b>
|
||||
// {devices.filter(a => a.kind === 'videoinput').map(device =>
|
||||
// <div key={device.deviceId}
|
||||
// onClick={() => setSelectedDevice((d: any) => {
|
||||
// return { ...d, video: { deviceId: { exact: device.deviceId } } }
|
||||
// })}
|
||||
// className={`${selectedDevice && selectedDevice.video && device.deviceId === selectedDevice?.video.deviceId.exact ? 'bg-green-300' : ''}`}
|
||||
// >
|
||||
// {device.label}
|
||||
// </div>
|
||||
// )}
|
||||
// <b>Audio</b>
|
||||
// {devices.filter(a => a.kind === 'audioinput').map(device =>
|
||||
// <div key={device.deviceId}
|
||||
// onClick={() => setSelectedDevice((d: any) => {
|
||||
// return { ...d, audio: { deviceId: { exact: device.deviceId } } }
|
||||
// })}
|
||||
// className={`${selectedDevice && selectedDevice.audio && device.deviceId === selectedDevice?.audio.deviceId.exact ? 'bg-green-300' : ''}`}
|
||||
|
||||
// >
|
||||
// {device.label}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// <input placeholder='Cannect to' onChange={e => setTheirPeerId(e.target.value)} />
|
||||
// <button onClick={() => {
|
||||
// console.log(`Connecting`)
|
||||
// connection.current
|
||||
// = peer.connect(theirPeerId, {
|
||||
|
||||
// });
|
||||
// }}>Start</button>
|
||||
// <button
|
||||
// className='p-2'
|
||||
// onClick={() => {
|
||||
// // const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
|
||||
// navigator.mediaDevices.getUserMedia(selectedDevice)
|
||||
// .then(stream => peer.call(theirPeerId, stream))
|
||||
// // const call = peer.call(theirPeerId, stream);
|
||||
|
||||
// // const stream = await navigator.mediaDevices.getUserMedia({
|
||||
// // video: { deviceId: { exact: selectedVideoId } }
|
||||
// // });
|
||||
// }}
|
||||
// >Call</button>
|
||||
// <textarea onChange={e => connection.current?.send(e.target.value)} placeholder='Message' rows={6} />
|
||||
// {JSON.stringify(received)}
|
||||
// <video ref={videoRef} />
|
||||
// </div>
|
||||
// )
|
||||
}
|
||||
|
||||
|
||||
export function Screen({ color, loading, children }: {
|
||||
color: 'red' | 'blue' | 'green' | 'orange',
|
||||
loading: boolean
|
||||
children: ReactNode
|
||||
}) {
|
||||
return <div
|
||||
style={{
|
||||
background: color == 'orange' ? '#441306' : color == 'green' ? '#032e15' : color == 'red' ? '#460809' : '#162456'
|
||||
}}
|
||||
className={`h-screen w-screen flex items-center align-middle justify-center`}>
|
||||
<>
|
||||
<div className='relative '>
|
||||
{/* <div className='absolute flex items-center align-middle justify-center -left-20 -top-10 bg-blue-900 hover:bg-blue-800 shadow shadow-white shadow-2xl rounded-full grow w-64 h-64'>
|
||||
ITEM 1
|
||||
</div>
|
||||
<div className='absolute right-0 bg-blue-950 hover:bg-blue-900 w-64 h-64'>
|
||||
ITEM 2
|
||||
</div>
|
||||
<div className='absolute bottom-0 bg-blue-950 hover:bg-blue-900'>
|
||||
ITEM 3
|
||||
</div> */}
|
||||
<div
|
||||
style={{
|
||||
background: color == 'orange' ?
|
||||
'radial-gradient(circle, oklch(40.8% 0.123 38.172) 0%, rgba(0,0,0,0.3) 100%)'
|
||||
: color == 'green' ?
|
||||
'radial-gradient(circle, oklch(39.3% 0.095 152.535) 0%, rgba(0,0,0,0.3) 100%)' :
|
||||
color == 'red' ?
|
||||
'radial-gradient(circle,oklch(39.6% 0.141 25.723) 0%, rgba(0,0,0,0.3) 100%)'
|
||||
:
|
||||
'radial-gradient(circle,oklch(37.9% 0.146 265.522) 0%, rgba(0,0,0,0.3) 100%)'
|
||||
|
||||
}}
|
||||
className={`w-96 h-95 rounded-full relative border-8 shadow-lg ${loading && `animate-spin shadow-purple-300/75`} text-3xl text-white flex items-center justify-center align-middle `}>
|
||||
<div className="absolute inset-0 flex items-center justify-center animate-[inherit] direction-reverse">
|
||||
<span className="flex gap-2 flex-col items-center font-bold text-white text-shadow-white text-shadow-2xs">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function ConnectingToSerever({
|
||||
setFlowState,
|
||||
}: {
|
||||
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
|
||||
}) {
|
||||
useEffect(() => { setFlowState(s => { return { ...s, color: 'blue', loading: true } }) }, [])
|
||||
|
||||
|
||||
// const peerConn = useCallback(() => {
|
||||
|
||||
// const peer = new Peer({
|
||||
// host: "localhost",
|
||||
// port: 4000,
|
||||
// path: "/myapp",
|
||||
// secure: false, // Use true with HTTPS
|
||||
// });
|
||||
// peer.on("open", function (id) {
|
||||
// setPeerId(id)
|
||||
// setColor('green')
|
||||
// console.log("My peer ID is: " + id);
|
||||
// });
|
||||
|
||||
// peer.on('error', function (err) {
|
||||
// setColor('red')
|
||||
// setLoading(false)
|
||||
// })
|
||||
|
||||
// peer.on('connection', function (conn) {
|
||||
// console.log(`Got Connection`, conn)
|
||||
// conn.on("data", function (data) {
|
||||
// // setReceivved(data as string)
|
||||
// console.log("Received", data);
|
||||
// });
|
||||
// conn.send("Hello!");
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
// useEffect(() => {
|
||||
// const load = async () => {
|
||||
// peer.on("open", function (id) {
|
||||
// setPeerId(id)
|
||||
// setColor('green')
|
||||
// console.log("My peer ID is: " + id);
|
||||
// });
|
||||
|
||||
// peer.on('error', function (err) {
|
||||
// setColor('red')
|
||||
// setLoading(false)
|
||||
// })
|
||||
|
||||
// peer.on('connection', function (conn) {
|
||||
// console.log(`Got Connection`, conn)
|
||||
// conn.on("data", function (data) {
|
||||
// // setReceivved(data as string)
|
||||
// console.log("Received", data);
|
||||
// });
|
||||
// conn.send("Hello!");
|
||||
// });
|
||||
|
||||
// }
|
||||
// setColor('green')
|
||||
// setLoading(true)
|
||||
// load()
|
||||
// }, [])
|
||||
return <>
|
||||
<Server size={45} />
|
||||
Connecting to Server
|
||||
</>
|
||||
}
|
||||
|
||||
export function SelectMicrophone({
|
||||
setFlowState,
|
||||
onChange
|
||||
}: {
|
||||
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
|
||||
onChange: (device: string) => void
|
||||
}) {
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
// Load media devices
|
||||
// setTimeout(() => setLoading(false), 3000)
|
||||
// setLoading(false)
|
||||
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
|
||||
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
|
||||
}
|
||||
setFlowState(s => { return { ...s, color: 'green', loading: true } })
|
||||
load()
|
||||
}, [])
|
||||
return <>
|
||||
<MicrochipIcon size={45} />
|
||||
{
|
||||
devices.length == 0 ? 'Finding Audio Devices' : <>
|
||||
|
||||
Select audio device
|
||||
<select className='text-sm w-64 bg-white p-2 text-black'
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
<option>Select</option>
|
||||
{devices.filter(a => a.kind === 'audioinput').map((device, i) =>
|
||||
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
|
||||
)}
|
||||
|
||||
</select>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export function SelectVideo({
|
||||
setFlowState,
|
||||
onChange
|
||||
}: {
|
||||
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
|
||||
onChange: (device: string) => void
|
||||
}) {
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
// Load media devices
|
||||
// setTimeout(() => setLoading(false), 3000)
|
||||
// setLoading(false)
|
||||
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
|
||||
}
|
||||
setFlowState(s => { return { ...s, color: 'green', loading: true } })
|
||||
load()
|
||||
}, [])
|
||||
return <>
|
||||
<VideoIcon size={45} />
|
||||
{
|
||||
devices.length == 0 ? 'Finding Video Devices' : <>
|
||||
|
||||
Select video device
|
||||
<select className='text-sm w-64 bg-white p-2 text-black'
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
<option>Select</option>
|
||||
{devices.filter(a => a.kind === 'videoinput').map((device, i) =>
|
||||
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
|
||||
)}
|
||||
|
||||
</select>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
538
webapp/components/chat/WebRTCChatUsingPeer.tsx
Normal file
@ -0,0 +1,538 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import { CameraIcon, HdIcon, MicrochipIcon, Server, VideoIcon } from 'lucide-react';
|
||||
import Peer, { DataConnection } from 'peerjs';
|
||||
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
type FlowStateType = {
|
||||
color: 'red' | 'blue' | 'green' | 'orange',
|
||||
flow: 'MIC' | 'VIDEO' | 'SERVER' | 'NO_PEERS' | 'ROOM',
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export default function WebRTCChatUsingPeer() {
|
||||
let mediaStream
|
||||
// UI
|
||||
|
||||
// const [flowSection, setFlowSection] = useState<'MIC' | 'VIDEO' | 'SERVER'>('MIC')
|
||||
const [flowState, setFlowState] = useState<FlowStateType>({
|
||||
color: 'blue',
|
||||
flow: 'SERVER',
|
||||
loading: false
|
||||
})
|
||||
|
||||
|
||||
// const [flow, setFlow] = useState<{
|
||||
// screen: ReactNode,
|
||||
// loading: boolean,
|
||||
// color: 'red' | 'blue' | 'green' | 'orange',
|
||||
// options?: any
|
||||
// }[]>([
|
||||
// // {
|
||||
// // loading: false,
|
||||
// // screen: <SelectMicrophone setLoading={setLoading} />,
|
||||
// // color: 'orange',
|
||||
// // options: [{
|
||||
// // label: 'Next',
|
||||
// // onSelect: (a: any) => { }
|
||||
// // }]
|
||||
// // },
|
||||
// {
|
||||
// loading: false,
|
||||
// screen: <>
|
||||
// <CameraIcon size={45} />
|
||||
// Select video device
|
||||
// <select>
|
||||
// <option>Select</option>
|
||||
// </select>
|
||||
// </>,
|
||||
// color: 'orange'
|
||||
// }, {
|
||||
// loading: true,
|
||||
// screen: <ConnectingToSerever />,
|
||||
// color: 'blue'
|
||||
// }])
|
||||
|
||||
|
||||
const peerRef = useRef<Peer>(undefined)
|
||||
const [peers, setPeers] = useState<string[]>([])
|
||||
|
||||
|
||||
const connection = useRef<DataConnection>(undefined)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
// const [mediaStream, setMediaStream] = useState<MediaStream>(null)
|
||||
const [received, setReceivved] = useState<string>('')
|
||||
const [peerId, setPeerId] = useState<string>('')
|
||||
const [removepeerId, setRemovePeerId] = useState<string>('')
|
||||
const [theirPeerId, setTheirPeerId] = useState<string>('')
|
||||
const [myStream, setMyStream] = useState<MediaStream|null>(null);
|
||||
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
const [selectedDevice, setSelectedDevice] = useState<{ video: { deviceId: { exact: string } }, audio: { deviceId: { exact: string } } }>()
|
||||
|
||||
const handleFindPeers = () => {
|
||||
peerRef.current?.listAllPeers(peers => {
|
||||
setPeers(() => peers)
|
||||
console.log(`PEERS`, peers)
|
||||
if (peers.length > 1) {
|
||||
console.log(`CONNECT TO: `, peers.filter(a => a !== peerId)[0])
|
||||
if (!connection.current) {
|
||||
connection.current = peerRef.current?.connect(peers.filter(a => a !== peerId)[0])
|
||||
setRemovePeerId(() => peers.filter(a => a !== peerId)[0])
|
||||
}
|
||||
setFlowState(s => { return { ...s, flow: 'ROOM', color: 'blue', loading: true } })
|
||||
} else {
|
||||
setFlowState(s => { return { ...s, flow: 'NO_PEERS', color: 'blue', loading: true } })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleConnectServer = () => {
|
||||
setFlowState(s => { return { ...s, flow: 'SERVER' } })
|
||||
|
||||
peerRef.current = new Peer({
|
||||
host: "localhost",
|
||||
port: 4000,
|
||||
path: "/myapp",
|
||||
secure: false, // Use true with HTTPS
|
||||
})
|
||||
|
||||
// peerRef.current.socket.on('message')
|
||||
peerRef.current.on("open", function (id) {
|
||||
setPeerId(id)
|
||||
setFlowState(s => { return { ...s, color: 'green', loading: false } })
|
||||
console.log("My peer ID is: " + id, 'Connected Peers:',);
|
||||
handleFindPeers()
|
||||
});
|
||||
|
||||
peerRef.current.on('error', function (err) {
|
||||
setFlowState(s => { return { ...s, color: 'red', loading: false } })
|
||||
})
|
||||
peerRef.current.on('close', function () {
|
||||
console.log(`Closing connection`)
|
||||
})
|
||||
peerRef.current.on('connection', function (conn) {
|
||||
console.log(`Got Connection`, conn)
|
||||
// handleFindPeers()
|
||||
setFlowState(s => { return { ...s, flow: 'ROOM' } })
|
||||
connection.current!.on("data", function (data) {
|
||||
// setReceivved(data as string)
|
||||
console.log("Received", data);
|
||||
});
|
||||
conn.send("Hello!");
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleConnectServer()
|
||||
}, [])
|
||||
|
||||
|
||||
// const peer = new Peer({
|
||||
// host: "localhost",
|
||||
// port: 4000,
|
||||
// path: "/myapp",
|
||||
// secure: false, // Use true with HTTPS
|
||||
// });
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// peer.on("open", function (id) {
|
||||
// setPeerId(id)
|
||||
// console.log("My peer ID is: " + id);
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
|
||||
// peer.on('connection', function (conn) {
|
||||
// console.log(`Got Connection`, conn)
|
||||
// conn.on("data", function (data) {
|
||||
// setReceivved(data as string)
|
||||
// console.log("Received", data);
|
||||
// });
|
||||
// conn.send("Hello!");
|
||||
// });
|
||||
// useEffect(() => {
|
||||
// // Get user media
|
||||
// navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// .then(stream => {
|
||||
// // setMyStream(stream);
|
||||
// // if (videoRef.current) {
|
||||
// // videoRef.current.srcObject = stream;
|
||||
// // }
|
||||
// })
|
||||
// .catch(err => console.error("Failed to get local stream", err));
|
||||
|
||||
// // Initialize PeerJS
|
||||
// // const peer = new Peer(); // PeerJS manages server connection
|
||||
// // currentPeer.current = peer;
|
||||
|
||||
// peer.on('open', (id) => {
|
||||
// setPeerId(id);
|
||||
// console.log('My peer ID is:', id);
|
||||
// });
|
||||
|
||||
// // Listen for incoming calls
|
||||
// peer.on('call', (call) => {
|
||||
// if (myStream) {
|
||||
// // Answer the call with your stream
|
||||
// call.answer(myStream);
|
||||
// call.on('stream', (remoteStream) => {
|
||||
// // Show stream in remote video element
|
||||
// if (videoRef.current) {
|
||||
// videoRef.current.srcObject = remoteStream;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// peer.on('error', (err) => console.error(err));
|
||||
|
||||
// return () => {
|
||||
// if (peer) {
|
||||
// peer.destroy();
|
||||
// }
|
||||
// };
|
||||
// }, [myStream]);
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// const load = async () => {
|
||||
// // Load media devices
|
||||
// const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
// setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
// // )
|
||||
// // setFlowIdx(1)
|
||||
// // setFlowIdx(2)
|
||||
|
||||
// // console.log(await navigator.mediaDevices.enumerateDevices())
|
||||
// // console.log(await navigator.mediaDevices.getUserMedia().)
|
||||
// }
|
||||
// load()
|
||||
// peer.on("call", function (call) {
|
||||
// alert(`You have a call`)
|
||||
// // Answer the call, providing our mediaStream
|
||||
// // if (mediaStream) {
|
||||
// call.answer(mediaStream);
|
||||
|
||||
// call.on('stream', (remoteStream) => {
|
||||
// // Show stream in remote video element
|
||||
// if (videoRef.current) {
|
||||
// videoRef.current.srcObject = remoteStream;
|
||||
// }
|
||||
// });
|
||||
// // }
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
if (flowState.flow === 'ROOM') {
|
||||
return <div className='flex flex-col w-screen h-screen'>
|
||||
<div className='flex justify-between w-full flex-col'>
|
||||
<div>LOCAL PEER:{peerId}</div>
|
||||
<div>REMOVE PEER:{removepeerId}</div>
|
||||
</div>
|
||||
<div className='grow'></div>
|
||||
<form onSubmit={(form) => {
|
||||
form.preventDefault()
|
||||
const formData = new FormData(form.target);
|
||||
// const det = Object.fromEntries(formData.entries())
|
||||
try {
|
||||
console.log(`Sending:`, connection.current, formData.get('message') as string)
|
||||
connection.current?.send(formData.get('message') as string)
|
||||
} catch (e) {
|
||||
console.error('Failed to send message', e)
|
||||
}
|
||||
}}>
|
||||
<input name='message' placeholder='message' />
|
||||
<button type='submit'>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
return (<Screen
|
||||
color={flowState.color}
|
||||
loading={flowState.loading}>
|
||||
{
|
||||
{
|
||||
'MIC': <SelectMicrophone setFlowState={setFlowState}
|
||||
onChange={e => {
|
||||
setSelectedDevice((d: any) => {
|
||||
return { ...d, audio: { deviceId: { exact: e } } }
|
||||
})
|
||||
setFlowState(s => { return { ...s, flow: 'VIDEO' } })
|
||||
// setFlowSection('VIDEO')
|
||||
}}
|
||||
/>,
|
||||
'VIDEO': <SelectVideo setFlowState={setFlowState}
|
||||
onChange={e => {
|
||||
setSelectedDevice((d: any) => {
|
||||
return { ...d, video: { deviceId: { exact: e } } }
|
||||
})
|
||||
handleConnectServer()
|
||||
}}
|
||||
/>,
|
||||
'SERVER': <ConnectingToSerever
|
||||
setFlowState={setFlowState}
|
||||
/>,
|
||||
'NO_PEERS': <>Waiting for others to join</>,
|
||||
'ROOM': <></>
|
||||
}[flowState.flow]
|
||||
}
|
||||
{/* {flow[flowIdx].screen} */}
|
||||
</Screen>)
|
||||
// return (
|
||||
// <div>
|
||||
// {JSON.stringify(peerId)}
|
||||
// <div>
|
||||
// <b>Settings</b>
|
||||
// <b>Video</b>
|
||||
// {devices.filter(a => a.kind === 'videoinput').map(device =>
|
||||
// <div key={device.deviceId}
|
||||
// onClick={() => setSelectedDevice((d: any) => {
|
||||
// return { ...d, video: { deviceId: { exact: device.deviceId } } }
|
||||
// })}
|
||||
// className={`${selectedDevice && selectedDevice.video && device.deviceId === selectedDevice?.video.deviceId.exact ? 'bg-green-300' : ''}`}
|
||||
// >
|
||||
// {device.label}
|
||||
// </div>
|
||||
// )}
|
||||
// <b>Audio</b>
|
||||
// {devices.filter(a => a.kind === 'audioinput').map(device =>
|
||||
// <div key={device.deviceId}
|
||||
// onClick={() => setSelectedDevice((d: any) => {
|
||||
// return { ...d, audio: { deviceId: { exact: device.deviceId } } }
|
||||
// })}
|
||||
// className={`${selectedDevice && selectedDevice.audio && device.deviceId === selectedDevice?.audio.deviceId.exact ? 'bg-green-300' : ''}`}
|
||||
|
||||
// >
|
||||
// {device.label}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// <input placeholder='Cannect to' onChange={e => setTheirPeerId(e.target.value)} />
|
||||
// <button onClick={() => {
|
||||
// console.log(`Connecting`)
|
||||
// connection.current
|
||||
// = peer.connect(theirPeerId, {
|
||||
|
||||
// });
|
||||
// }}>Start</button>
|
||||
// <button
|
||||
// className='p-2'
|
||||
// onClick={() => {
|
||||
// // const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
|
||||
// navigator.mediaDevices.getUserMedia(selectedDevice)
|
||||
// .then(stream => peer.call(theirPeerId, stream))
|
||||
// // const call = peer.call(theirPeerId, stream);
|
||||
|
||||
// // const stream = await navigator.mediaDevices.getUserMedia({
|
||||
// // video: { deviceId: { exact: selectedVideoId } }
|
||||
// // });
|
||||
// }}
|
||||
// >Call</button>
|
||||
// <textarea onChange={e => connection.current?.send(e.target.value)} placeholder='Message' rows={6} />
|
||||
// {JSON.stringify(received)}
|
||||
// <video ref={videoRef} />
|
||||
// </div>
|
||||
// )
|
||||
}
|
||||
|
||||
|
||||
export function Screen({ color, loading, children }: {
|
||||
color: 'red' | 'blue' | 'green' | 'orange',
|
||||
loading: boolean
|
||||
children: ReactNode
|
||||
}) {
|
||||
return <div
|
||||
style={{
|
||||
background: color == 'orange' ? '#441306' : color == 'green' ? '#032e15' : color == 'red' ? '#460809' : '#162456'
|
||||
}}
|
||||
className={`h-screen w-screen flex items-center align-middle justify-center`}>
|
||||
<>
|
||||
<div className='relative '>
|
||||
{/* <div className='absolute flex items-center align-middle justify-center -left-20 -top-10 bg-blue-900 hover:bg-blue-800 shadow shadow-white shadow-2xl rounded-full grow w-64 h-64'>
|
||||
ITEM 1
|
||||
</div>
|
||||
<div className='absolute right-0 bg-blue-950 hover:bg-blue-900 w-64 h-64'>
|
||||
ITEM 2
|
||||
</div>
|
||||
<div className='absolute bottom-0 bg-blue-950 hover:bg-blue-900'>
|
||||
ITEM 3
|
||||
</div> */}
|
||||
<div
|
||||
style={{
|
||||
background: color == 'orange' ?
|
||||
'radial-gradient(circle, oklch(40.8% 0.123 38.172) 0%, rgba(0,0,0,0.3) 100%)'
|
||||
: color == 'green' ?
|
||||
'radial-gradient(circle, oklch(39.3% 0.095 152.535) 0%, rgba(0,0,0,0.3) 100%)' :
|
||||
color == 'red' ?
|
||||
'radial-gradient(circle,oklch(39.6% 0.141 25.723) 0%, rgba(0,0,0,0.3) 100%)'
|
||||
:
|
||||
'radial-gradient(circle,oklch(37.9% 0.146 265.522) 0%, rgba(0,0,0,0.3) 100%)'
|
||||
|
||||
}}
|
||||
className={`w-96 h-95 rounded-full relative border-8 shadow-lg ${loading && `animate-spin shadow-purple-300/75`} text-3xl text-white flex items-center justify-center align-middle `}>
|
||||
<div className="absolute inset-0 flex items-center justify-center animate-[inherit] direction-reverse">
|
||||
<span className="flex gap-2 flex-col items-center font-bold text-white text-shadow-white text-shadow-2xs">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function ConnectingToSerever({
|
||||
setFlowState,
|
||||
}: {
|
||||
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
|
||||
}) {
|
||||
useEffect(() => { setFlowState(s => { return { ...s, color: 'blue', loading: true } }) }, [])
|
||||
|
||||
|
||||
// const peerConn = useCallback(() => {
|
||||
|
||||
// const peer = new Peer({
|
||||
// host: "localhost",
|
||||
// port: 4000,
|
||||
// path: "/myapp",
|
||||
// secure: false, // Use true with HTTPS
|
||||
// });
|
||||
// peer.on("open", function (id) {
|
||||
// setPeerId(id)
|
||||
// setColor('green')
|
||||
// console.log("My peer ID is: " + id);
|
||||
// });
|
||||
|
||||
// peer.on('error', function (err) {
|
||||
// setColor('red')
|
||||
// setLoading(false)
|
||||
// })
|
||||
|
||||
// peer.on('connection', function (conn) {
|
||||
// console.log(`Got Connection`, conn)
|
||||
// conn.on("data", function (data) {
|
||||
// // setReceivved(data as string)
|
||||
// console.log("Received", data);
|
||||
// });
|
||||
// conn.send("Hello!");
|
||||
// });
|
||||
// }, [])
|
||||
|
||||
// useEffect(() => {
|
||||
// const load = async () => {
|
||||
// peer.on("open", function (id) {
|
||||
// setPeerId(id)
|
||||
// setColor('green')
|
||||
// console.log("My peer ID is: " + id);
|
||||
// });
|
||||
|
||||
// peer.on('error', function (err) {
|
||||
// setColor('red')
|
||||
// setLoading(false)
|
||||
// })
|
||||
|
||||
// peer.on('connection', function (conn) {
|
||||
// console.log(`Got Connection`, conn)
|
||||
// conn.on("data", function (data) {
|
||||
// // setReceivved(data as string)
|
||||
// console.log("Received", data);
|
||||
// });
|
||||
// conn.send("Hello!");
|
||||
// });
|
||||
|
||||
// }
|
||||
// setColor('green')
|
||||
// setLoading(true)
|
||||
// load()
|
||||
// }, [])
|
||||
return <>
|
||||
<Server size={45} />
|
||||
Connecting to Server
|
||||
</>
|
||||
}
|
||||
|
||||
export function SelectMicrophone({
|
||||
setFlowState,
|
||||
onChange
|
||||
}: {
|
||||
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
|
||||
onChange: (device: string) => void
|
||||
}) {
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
// Load media devices
|
||||
// setTimeout(() => setLoading(false), 3000)
|
||||
// setLoading(false)
|
||||
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
|
||||
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
|
||||
}
|
||||
setFlowState(s => { return { ...s, color: 'green', loading: true } })
|
||||
load()
|
||||
}, [])
|
||||
return <>
|
||||
<MicrochipIcon size={45} />
|
||||
{
|
||||
devices.length == 0 ? 'Finding Audio Devices' : <>
|
||||
|
||||
Select audio device
|
||||
<select className='text-sm w-64 bg-white p-2 text-black'
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
<option>Select</option>
|
||||
{devices.filter(a => a.kind === 'audioinput').map((device, i) =>
|
||||
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
|
||||
)}
|
||||
|
||||
</select>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export function SelectVideo({
|
||||
setFlowState,
|
||||
onChange
|
||||
}: {
|
||||
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
|
||||
onChange: (device: string) => void
|
||||
}) {
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
// Load media devices
|
||||
// setTimeout(() => setLoading(false), 3000)
|
||||
// setLoading(false)
|
||||
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
// // .then(async res =>
|
||||
setDevices(await navigator.mediaDevices.enumerateDevices())
|
||||
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
|
||||
}
|
||||
setFlowState(s => { return { ...s, color: 'green', loading: true } })
|
||||
load()
|
||||
}, [])
|
||||
return <>
|
||||
<VideoIcon size={45} />
|
||||
{
|
||||
devices.length == 0 ? 'Finding Video Devices' : <>
|
||||
|
||||
Select video device
|
||||
<select className='text-sm w-64 bg-white p-2 text-black'
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
<option>Select</option>
|
||||
{devices.filter(a => a.kind === 'videoinput').map((device, i) =>
|
||||
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
|
||||
)}
|
||||
|
||||
</select>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
197
webapp/components/chat/WebRTComponent.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
'use client'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
// https://www.baeldung.com/webrtc
|
||||
export default function WebRTComponent() {
|
||||
const WSRef = useRef<WebSocket>(undefined)
|
||||
const videoRef1 = useRef<HTMLVideoElement | null>(null)
|
||||
const localStreamRef1 = useRef<MediaStream | null>(null)
|
||||
const [offers, setOffers] = useState<RTCSessionDescriptionInit[]>([])
|
||||
const peerConnection = useRef<RTCPeerConnection>(undefined)
|
||||
const dataChannel = useRef<RTCDataChannel>(undefined)
|
||||
|
||||
const send = (message: any) =>
|
||||
WSRef.current!.send(JSON.stringify(message));
|
||||
|
||||
const onWSMessage = async (message: MessageEvent) => {
|
||||
console.log(`Received : `, message.data)
|
||||
const parsed = JSON.parse(message.data)
|
||||
switch (parsed.type) {
|
||||
case 'Offers':
|
||||
// setOffers(o => parsed.data as RTCSessionDescriptionInit[])
|
||||
console.log('parsed.data',parsed.data)
|
||||
await acceptOffer(parsed.data)
|
||||
// await acceptOffer(parsed.data[parsed.data.length - 1])
|
||||
break;
|
||||
case 'Answers':
|
||||
console.log(`Answers:`,parsed.data)
|
||||
peerConnection.current!
|
||||
.setRemoteDescription(new RTCSessionDescription({ ...parsed.data.sdp }))
|
||||
.catch((err) => console.error(err)); //
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const handleConectWS = async () => {
|
||||
WSRef.current = new WebSocket('ws://localhost:4000/socket');
|
||||
WSRef.current.onopen = async function (event) {
|
||||
peerConnection.current = new RTCPeerConnection(undefined) // <-- Later add Stun servers as backup
|
||||
await setup()
|
||||
}
|
||||
WSRef.current.onmessage = async (ev) => onWSMessage(ev)
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 1. Get user media
|
||||
const Start = async () => {
|
||||
console.log('Requesting local stream');
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
console.log(`Start Local Stream`)
|
||||
if (!videoRef1.current) console.error(`Video not yet available`)
|
||||
videoRef1.current!.srcObject = stream
|
||||
localStreamRef1.current = stream;
|
||||
console.log(`Set local stream`)
|
||||
}
|
||||
|
||||
const joinServer = async () => {
|
||||
const configuration = {};
|
||||
console.log('RTCPeerConnection configuration:', configuration);
|
||||
peerConnection.current = new RTCPeerConnection(configuration)
|
||||
|
||||
peerConnection.current.createOffer().then(offer => {
|
||||
send({
|
||||
type: 'OfferSDP',
|
||||
data: offer
|
||||
})
|
||||
peerConnection.current?.setLocalDescription(offer)
|
||||
})
|
||||
|
||||
peerConnection.current.onicecandidate = function (event) {
|
||||
console.log(`Received ICE Candidate :`, event)
|
||||
}
|
||||
// pc.addEventListener('icecandidate', e => console.log(`icecandidate`, pc, e));
|
||||
}
|
||||
|
||||
const acceptOffer = async (recv: any) => {
|
||||
console.log(`acceptOffer`, recv)
|
||||
peerConnection.current!.setRemoteDescription(new RTCSessionDescription(recv))
|
||||
.then(() => peerConnection.current!.createAnswer())
|
||||
.then((answer) => peerConnection.current!.setLocalDescription(answer))
|
||||
.then(() => send({ type: 'AnswerSDP', data: { sdp: peerConnection.current!.localDescription } }))
|
||||
.catch((err) => console.error('Failed to handle offer', err));
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 2. Make call
|
||||
const makeCall = () => {
|
||||
// Get available streams
|
||||
const videoTracks = localStreamRef1.current!.getVideoTracks();
|
||||
const audioTracks = localStreamRef1.current!.getAudioTracks();
|
||||
|
||||
if (videoTracks.length > 0) {
|
||||
console.log(`Using video device: ${videoTracks[0].label}`);
|
||||
}
|
||||
if (audioTracks.length > 0) {
|
||||
console.log(`Using audio device: ${audioTracks[0].label}`);
|
||||
}
|
||||
const configuration = {};
|
||||
|
||||
// 3. Start WebRTC
|
||||
console.log('RTCPeerConnection configuration:', configuration);
|
||||
|
||||
// const pc1 = new RTCPeerConnection(configuration);
|
||||
// console.log('Created local peer connection object pc1');
|
||||
// pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e));
|
||||
// const pc2 = new RTCPeerConnection(configuration);
|
||||
// console.log('Created remote peer connection object pc2');
|
||||
// pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
|
||||
// pc1.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1, e));
|
||||
// pc2.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2, e));
|
||||
// pc2.addEventListener('track', gotRemoteStream);
|
||||
|
||||
}
|
||||
|
||||
// 4. On ICE Candidate
|
||||
// const onIceCandidate = async (peerConnection: RTCPeerConnection, e: Event) => {
|
||||
// try {
|
||||
// await(getOtherPc(pc).addIceCandidate(event.candidate));
|
||||
// onAddIceCandidateSuccess(pc);
|
||||
// } catch (e) {
|
||||
// onAddIceCandidateError(pc, e);
|
||||
// }
|
||||
// console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
|
||||
// }
|
||||
|
||||
async function setup() {
|
||||
// 1. Get local media stream
|
||||
// await Start()
|
||||
// 2. Join WS Server
|
||||
const handleJoin = () => {
|
||||
send({ type: 'Join' })
|
||||
}
|
||||
handleJoin()
|
||||
}
|
||||
useEffect(() => {
|
||||
handleConectWS()
|
||||
// const load = async () => {
|
||||
// WSRef.current.onmessage = (ev) => {
|
||||
// // '{"event":"candidate","data":{"candidate":"candidate:1041930304 1 udp 2113937151 b7178aee-2d1d-4762-b709-1b25cf436cb5.local 61551 typ host generation 0 ufrag bEe6 network-cost 999","sdpMid":"0","sdpMLineIndex":0,"usernameFragment":"bEe6"}}'
|
||||
// const data = JSON.parse(ev.data)
|
||||
// // console.log(`Recevied WS : `, Object.keys(data))
|
||||
// console.log(`Recevied WS : `, data.event)
|
||||
// switch (data.event) {
|
||||
// case 'offer':
|
||||
// console.log(`Check foreign offer`, data.data)
|
||||
// // Need to handle answer here
|
||||
// peerConnection.current.setRemoteDescription(new RTCSessionDescription({ sdp: data.data.sdp, type: 'offer' }))
|
||||
// .then(() => peerConnection.current.createAnswer())
|
||||
// .then((answer) => peerConnection.current.setLocalDescription(answer))
|
||||
// .then(() => send({ event: 'answer', data: { sdp: peerConnection.current.localDescription } }))
|
||||
// .catch((err) => console.error('Failed to handle offer', err));
|
||||
// break;
|
||||
// case 'answer':
|
||||
// console.log(`Check foreign answer`, data.data)
|
||||
// peerConnection.current.setRemoteDescription(new RTCSessionDescription({ ...data.data.sdp })).catch((err) => console.error(err)); //
|
||||
// break;
|
||||
// case 'candidate':
|
||||
// console.log(`Check foreign candidate`, { ...data.data })
|
||||
// if (data.data.candidate) {
|
||||
// peerConnection.current.addIceCandidate(new RTCIceCandidate({ ...data.data })).catch((err) => console.warn('Bad candidate', err));
|
||||
// }
|
||||
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// load()
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>WebRTCChat
|
||||
<video ref={videoRef1} playsInline autoPlay muted className='border w-96 h-96 bg-red-100' />
|
||||
<button onClick={() => Start()}>Start</button>
|
||||
<button onClick={() => joinServer()}>Join</button>
|
||||
<button onClick={() => makeCall()}>Make Call</button>
|
||||
<button onClick={() => {
|
||||
{ alert('SEND'); dataChannel.current!.send("message"); }
|
||||
}} >Send</button>
|
||||
|
||||
<div>
|
||||
<b>Offers</b>
|
||||
{/* <div className='flex flex-col'>
|
||||
{offers.map((o, i) => <div key={`Offer-${i}`}>
|
||||
{JSON.stringify(o.sdp)}
|
||||
<button onClick={() => {
|
||||
acceptOffer(o.sdp ?? "")
|
||||
}}>Connect</button>
|
||||
</div>)}
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
214
webapp/components/chat/main.js
Normal file
@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const startButton = document.getElementById('startButton');
|
||||
const callButton = document.getElementById('callButton');
|
||||
const hangupButton = document.getElementById('hangupButton');
|
||||
callButton.disabled = true;
|
||||
hangupButton.disabled = true;
|
||||
startButton.addEventListener('click', start);
|
||||
callButton.addEventListener('click', call);
|
||||
hangupButton.addEventListener('click', hangup);
|
||||
|
||||
let startTime;
|
||||
const localVideo = document.getElementById('localVideo');
|
||||
const remoteVideo = document.getElementById('remoteVideo');
|
||||
|
||||
localVideo.addEventListener('loadedmetadata', function() {
|
||||
console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`);
|
||||
});
|
||||
|
||||
remoteVideo.addEventListener('loadedmetadata', function() {
|
||||
console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`);
|
||||
});
|
||||
|
||||
remoteVideo.addEventListener('resize', () => {
|
||||
console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight} - Time since pageload ${performance.now().toFixed(0)}ms`);
|
||||
// We'll use the first onsize callback as an indication that video has started
|
||||
// playing out.
|
||||
if (startTime) {
|
||||
const elapsedTime = window.performance.now() - startTime;
|
||||
console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms');
|
||||
startTime = null;
|
||||
}
|
||||
});
|
||||
|
||||
let localStream;
|
||||
let pc1;
|
||||
let pc2;
|
||||
const offerOptions = {
|
||||
offerToReceiveAudio: 1,
|
||||
offerToReceiveVideo: 1
|
||||
};
|
||||
|
||||
function getName(pc) {
|
||||
return (pc === pc1) ? 'pc1' : 'pc2';
|
||||
}
|
||||
|
||||
function getOtherPc(pc) {
|
||||
return (pc === pc1) ? pc2 : pc1;
|
||||
}
|
||||
|
||||
async function start() {
|
||||
console.log('Requesting local stream');
|
||||
startButton.disabled = true;
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
|
||||
console.log('Received local stream');
|
||||
localVideo.srcObject = stream;
|
||||
localStream = stream;
|
||||
callButton.disabled = false;
|
||||
} catch (e) {
|
||||
alert(`getUserMedia() error: ${e.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function call() {
|
||||
callButton.disabled = true;
|
||||
hangupButton.disabled = false;
|
||||
console.log('Starting call');
|
||||
startTime = window.performance.now();
|
||||
const videoTracks = localStream.getVideoTracks();
|
||||
const audioTracks = localStream.getAudioTracks();
|
||||
if (videoTracks.length > 0) {
|
||||
console.log(`Using video device: ${videoTracks[0].label}`);
|
||||
}
|
||||
if (audioTracks.length > 0) {
|
||||
console.log(`Using audio device: ${audioTracks[0].label}`);
|
||||
}
|
||||
const configuration = {};
|
||||
console.log('RTCPeerConnection configuration:', configuration);
|
||||
pc1 = new RTCPeerConnection(configuration);
|
||||
console.log('Created local peer connection object pc1');
|
||||
pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e));
|
||||
pc2 = new RTCPeerConnection(configuration);
|
||||
console.log('Created remote peer connection object pc2');
|
||||
pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
|
||||
pc1.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1, e));
|
||||
pc2.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2, e));
|
||||
pc2.addEventListener('track', gotRemoteStream);
|
||||
|
||||
localStream.getTracks().forEach(track => pc1.addTrack(track, localStream));
|
||||
console.log('Added local stream to pc1');
|
||||
|
||||
try {
|
||||
console.log('pc1 createOffer start');
|
||||
const offer = await pc1.createOffer(offerOptions);
|
||||
await onCreateOfferSuccess(offer);
|
||||
} catch (e) {
|
||||
onCreateSessionDescriptionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onCreateSessionDescriptionError(error) {
|
||||
console.log(`Failed to create session description: ${error.toString()}`);
|
||||
}
|
||||
|
||||
async function onCreateOfferSuccess(desc) {
|
||||
console.log(`Offer from pc1\n${desc.sdp}`);
|
||||
console.log('pc1 setLocalDescription start');
|
||||
try {
|
||||
await pc1.setLocalDescription(desc);
|
||||
onSetLocalSuccess(pc1);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError();
|
||||
}
|
||||
|
||||
console.log('pc2 setRemoteDescription start');
|
||||
try {
|
||||
await pc2.setRemoteDescription(desc);
|
||||
onSetRemoteSuccess(pc2);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError();
|
||||
}
|
||||
|
||||
console.log('pc2 createAnswer start');
|
||||
// Since the 'remote' side has no media stream we need
|
||||
// to pass in the right constraints in order for it to
|
||||
// accept the incoming offer of audio and video.
|
||||
try {
|
||||
const answer = await pc2.createAnswer();
|
||||
await onCreateAnswerSuccess(answer);
|
||||
} catch (e) {
|
||||
onCreateSessionDescriptionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onSetLocalSuccess(pc) {
|
||||
console.log(`${getName(pc)} setLocalDescription complete`);
|
||||
}
|
||||
|
||||
function onSetRemoteSuccess(pc) {
|
||||
console.log(`${getName(pc)} setRemoteDescription complete`);
|
||||
}
|
||||
|
||||
function onSetSessionDescriptionError(error) {
|
||||
console.log(`Failed to set session description: ${error.toString()}`);
|
||||
}
|
||||
|
||||
function gotRemoteStream(e) {
|
||||
if (remoteVideo.srcObject !== e.streams[0]) {
|
||||
remoteVideo.srcObject = e.streams[0];
|
||||
console.log('pc2 received remote stream');
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateAnswerSuccess(desc) {
|
||||
console.log(`Answer from pc2:\n${desc.sdp}`);
|
||||
console.log('pc2 setLocalDescription start');
|
||||
try {
|
||||
await pc2.setLocalDescription(desc);
|
||||
onSetLocalSuccess(pc2);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
console.log('pc1 setRemoteDescription start');
|
||||
try {
|
||||
await pc1.setRemoteDescription(desc);
|
||||
onSetRemoteSuccess(pc1);
|
||||
} catch (e) {
|
||||
onSetSessionDescriptionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onIceCandidate(pc, event) {
|
||||
try {
|
||||
await (getOtherPc(pc).addIceCandidate(event.candidate));
|
||||
onAddIceCandidateSuccess(pc);
|
||||
} catch (e) {
|
||||
onAddIceCandidateError(pc, e);
|
||||
}
|
||||
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
|
||||
}
|
||||
|
||||
function onAddIceCandidateSuccess(pc) {
|
||||
console.log(`${getName(pc)} addIceCandidate success`);
|
||||
}
|
||||
|
||||
function onAddIceCandidateError(pc, error) {
|
||||
console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);
|
||||
}
|
||||
|
||||
function onIceStateChange(pc, event) {
|
||||
if (pc) {
|
||||
console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`);
|
||||
console.log('ICE state change event: ', event);
|
||||
}
|
||||
}
|
||||
|
||||
function hangup() {
|
||||
console.log('Ending call');
|
||||
pc1.close();
|
||||
pc2.close();
|
||||
pc1 = null;
|
||||
pc2 = null;
|
||||
hangupButton.disabled = true;
|
||||
callButton.disabled = false;
|
||||
}
|
||||
6
webapp/components/chat/socket.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
// "undefined" means the URL will be computed from the window.location object
|
||||
const URL = process.env.NODE_ENV === 'production' ? 'http://192.168.0.131:4000' : 'http://192.168.0.131:4000';
|
||||
|
||||
export const socket = io(URL);
|
||||
72
webapp/components/login-form.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { LoginFormAction } from "@/actions/auth/AuthActions"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Login to your account</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action={LoginFormAction}>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" name="password" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
<Button variant="outline" type="button">
|
||||
Login with Passkey
|
||||
</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account? <a href="/signup">Sign up</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
76
webapp/components/signup-form.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
export function SignupForm({ ...props }: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<CardTitle>Create an account</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your information below to create your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">Full Name</FieldLabel>
|
||||
<Input id="name" type="text" placeholder="John Doe" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
<FieldDescription>
|
||||
We'll use this to contact you. We will not share your email
|
||||
with anyone else.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<Input id="password" type="password" required />
|
||||
<FieldDescription>
|
||||
Must be at least 8 characters long.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="confirm-password">
|
||||
Confirm Password
|
||||
</FieldLabel>
|
||||
<Input id="confirm-password" type="password" required />
|
||||
<FieldDescription>Please confirm your password.</FieldDescription>
|
||||
</Field>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<Button type="submit">Create Account</Button>
|
||||
<Button variant="outline" type="button">
|
||||
Sign up with Google
|
||||
</Button>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
Already have an account? <a href="/login">Sign in</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
67
webapp/components/ui/button.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
103
webapp/components/ui/card.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
238
webapp/components/ui/field.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||
return (
|
||||
<fieldset
|
||||
data-slot="field-set"
|
||||
className={cn(
|
||||
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldLegend({
|
||||
className,
|
||||
variant = "legend",
|
||||
...props
|
||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
||||
return (
|
||||
<legend
|
||||
data-slot="field-legend"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-group"
|
||||
className={cn(
|
||||
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const fieldVariants = cva(
|
||||
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
|
||||
horizontal:
|
||||
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
responsive:
|
||||
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Field({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="field"
|
||||
data-orientation={orientation}
|
||||
className={cn(fieldVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-content"
|
||||
className={cn(
|
||||
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Label>) {
|
||||
return (
|
||||
<Label
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
|
||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
||||
"last:mt-0 nth-last-2:-mt-1",
|
||||
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-separator"
|
||||
data-content={!!children}
|
||||
className={cn(
|
||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Separator className="absolute inset-0 top-1/2" />
|
||||
{children && (
|
||||
<span
|
||||
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
||||
data-slot="field-separator-content"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldError({
|
||||
className,
|
||||
children,
|
||||
errors,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
errors?: Array<{ message?: string } | undefined>
|
||||
}) {
|
||||
const content = useMemo(() => {
|
||||
if (children) {
|
||||
return children
|
||||
}
|
||||
|
||||
if (!errors?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const uniqueErrors = [
|
||||
...new Map(errors.map((error) => [error?.message, error])).values(),
|
||||
]
|
||||
|
||||
if (uniqueErrors?.length == 1) {
|
||||
return uniqueErrors[0]?.message
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||
{uniqueErrors.map(
|
||||
(error, index) =>
|
||||
error?.message && <li key={index}>{error.message}</li>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}, [children, errors])
|
||||
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
className={cn("text-sm font-normal text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldContent,
|
||||
FieldTitle,
|
||||
}
|
||||
156
webapp/components/ui/input-group.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
"inline-start":
|
||||
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
|
||||
"inline-end":
|
||||
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
|
||||
"block-start":
|
||||
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
|
||||
"block-end":
|
||||
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function InputGroupAddon({
|
||||
className,
|
||||
align = "inline-start",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
data-align={align}
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
onClick={(e) => {
|
||||
if ((e.target as HTMLElement).closest("button")) {
|
||||
return
|
||||
}
|
||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const inputGroupButtonVariants = cva(
|
||||
"flex items-center gap-2 text-sm shadow-none",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
||||
sm: "",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
|
||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xs",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = "button",
|
||||
variant = "ghost",
|
||||
size = "xs",
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
||||
VariantProps<typeof inputGroupButtonVariants>) {
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupTextarea({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupInput,
|
||||
InputGroupTextarea,
|
||||
}
|
||||
19
webapp/components/ui/input.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
24
webapp/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Label as LabelPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
28
webapp/components/ui/separator.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
18
webapp/components/ui/textarea.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
18
webapp/eslint.config.mjs
Normal file
@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
25
webapp/helper/api/ApiConnector.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use server";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const _URL = `http://localhost:8081/api`;
|
||||
export async function CallApi<T>(
|
||||
endPoint: string,
|
||||
method: "GET" | "POST",
|
||||
body?: any,
|
||||
){
|
||||
const session = (await cookies()).get("ccsession")?.value ?? ("" as string);
|
||||
|
||||
const res = await fetch(`${_URL}${endPoint}`, {
|
||||
method,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${session}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}).catch((e) => console.error(e));
|
||||
if (!((res as Response).status === 200))
|
||||
throw Error(JSON.stringify(await (res as Response).json()));
|
||||
return await (res as Response).json();
|
||||
}
|
||||
6
webapp/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
14
webapp/next.config.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: "10mb",
|
||||
},
|
||||
viewTransition: true
|
||||
},
|
||||
output: 'standalone'
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
11875
webapp/package-lock.json
generated
Normal file
38
webapp/package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "webapp",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"mediasoup-client": "^3.18.7",
|
||||
"next": "^16.2.1",
|
||||
"peerjs": "^1.5.5",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"sdp-transform": "^3.0.0",
|
||||
"shadcn": "^4.1.0",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
7
webapp/postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
webapp/public/apple-icon-180.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
1
webapp/public/file.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
webapp/public/globe.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
webapp/public/manifest-icon-192 (1).png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
webapp/public/manifest-icon-192 (2).png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
webapp/public/manifest-icon-192.maskable.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
webapp/public/manifest-icon-192.maskable.png.kra
Normal file
BIN
webapp/public/manifest-icon-512.maskable.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
webapp/public/manifest-icon-96.maskable.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
webapp/public/manifest-icon-96.maskable.png.kra
Normal file
60
webapp/public/manifest.backup
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "CoffeeChat",
|
||||
"short_name": "CoffeeChat",
|
||||
"start_url": "chat.rootbranch.co.za",
|
||||
"display": "standalone",
|
||||
"description": "Start chatting with your communities",
|
||||
"lang": " en",
|
||||
"dir": "auto",
|
||||
"theme_color": "#59168b",
|
||||
"background_color": "#59168b",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/manifest-icon-192.maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/manifest-icon-192.maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/manifest-icon-512.maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/manifest-icon-512.maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "https://www.pwabuilder.com/assets/screenshots/screen1.png",
|
||||
"sizes": "2880x1800",
|
||||
"type": "image/png",
|
||||
"description": "Communities Home Page"
|
||||
}
|
||||
],
|
||||
"related_applications": [
|
||||
{
|
||||
"platform": "windows",
|
||||
"url": "https://chat.rootbranch.co.za"
|
||||
}
|
||||
],
|
||||
"prefer_related_applications": false,
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "CoffeeChat",
|
||||
"url": "chat.rootbranch.co.za",
|
||||
"description": "Start chatting with your communities"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
webapp/public/next.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
webapp/public/screen1.png
Normal file
|
After Width: | Height: | Size: 920 KiB |
BIN
webapp/public/screen1.png.kra
Normal file
BIN
webapp/public/screen2.png
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
webapp/public/screen2.png-autosave.kra
Normal file
93
webapp/public/service-worker.js
Normal file
@ -0,0 +1,93 @@
|
||||
|
||||
// Based off of https://github.com/pwa-builder/PWABuilder/blob/main/docs/sw.js
|
||||
|
||||
/*
|
||||
Welcome to our basic Service Worker! This Service Worker offers a basic offline experience
|
||||
while also being easily customizeable. You can add in your own code to implement the capabilities
|
||||
listed below, or change anything else you would like.
|
||||
|
||||
|
||||
Need an introduction to Service Workers? Check our docs here: https://docs.pwabuilder.com/#/home/sw-intro
|
||||
Want to learn more about how our Service Worker generation works? Check our docs here: https://docs.pwabuilder.com/#/studio/existing-app?id=add-a-service-worker
|
||||
|
||||
Did you know that Service Workers offer many more capabilities than just offline?
|
||||
- Background Sync: https://microsoft.github.io/win-student-devs/#/30DaysOfPWA/advanced-capabilities/06
|
||||
- Periodic Background Sync: https://web.dev/periodic-background-sync/
|
||||
- Push Notifications: https://microsoft.github.io/win-student-devs/#/30DaysOfPWA/advanced-capabilities/07?id=push-notifications-on-the-web
|
||||
- Badges: https://microsoft.github.io/win-student-devs/#/30DaysOfPWA/advanced-capabilities/07?id=application-badges
|
||||
*/
|
||||
|
||||
const HOSTNAME_WHITELIST = [
|
||||
self.location.hostname,
|
||||
'fonts.gstatic.com',
|
||||
'fonts.googleapis.com',
|
||||
'cdn.jsdelivr.net'
|
||||
]
|
||||
|
||||
// The Util Function to hack URLs of intercepted requests
|
||||
const getFixedUrl = (req) => {
|
||||
var now = Date.now()
|
||||
var url = new URL(req.url)
|
||||
|
||||
// 1. fixed http URL
|
||||
// Just keep syncing with location.protocol
|
||||
// fetch(httpURL) belongs to active mixed content.
|
||||
// And fetch(httpRequest) is not supported yet.
|
||||
url.protocol = self.location.protocol
|
||||
|
||||
// 2. add query for caching-busting.
|
||||
// Github Pages served with Cache-Control: max-age=600
|
||||
// max-age on mutable content is error-prone, with SW life of bugs can even extend.
|
||||
// Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
|
||||
// Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
|
||||
if (url.hostname === self.location.hostname) {
|
||||
url.search += (url.search ? '&' : '?') + 'cache-bust=' + now
|
||||
}
|
||||
return url.href
|
||||
}
|
||||
|
||||
/**
|
||||
* @Lifecycle Activate
|
||||
* New one activated when old isnt being used.
|
||||
*
|
||||
* waitUntil(): activating ====> activated
|
||||
*/
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
/**
|
||||
* @Functional Fetch
|
||||
* All network requests are being intercepted here.
|
||||
*
|
||||
* void respondWith(Promise<Response> r)
|
||||
*/
|
||||
self.addEventListener('fetch', event => {
|
||||
// Skip some of cross-origin requests, like those for Google Analytics.
|
||||
// if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {
|
||||
// // Stale-while-revalidate
|
||||
// // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
|
||||
// // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
|
||||
// const cached = caches.match(event.request)
|
||||
// const fixedUrl = getFixedUrl(event.request)
|
||||
// const fetched = fetch(fixedUrl, { cache: 'no-store' })
|
||||
// const fetchedCopy = fetched.then(resp => resp.clone())
|
||||
|
||||
// // Call respondWith() with whatever we get first.
|
||||
// // If the fetch fails (e.g disconnected), wait for the cache.
|
||||
// // If there’s nothing in cache, wait for the fetch.
|
||||
// // If neither yields a response, return offline pages.
|
||||
// event.respondWith(
|
||||
// Promise.race([fetched.catch(_ => cached), cached])
|
||||
// .then(resp => resp || fetched)
|
||||
// .catch(_ => { /* eat any errors */ })
|
||||
// )
|
||||
|
||||
// // Update the cache with the version we fetched (only for ok status)
|
||||
// event.waitUntil(
|
||||
// Promise.all([fetchedCopy, caches.open("pwa-cache")])
|
||||
// .then(([response, cache]) => response.ok && cache.put(event.request, response))
|
||||
// .catch(_ => { /* eat any errors */ })
|
||||
// )
|
||||
// }
|
||||
})
|
||||
1
webapp/public/vercel.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
webapp/public/window.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
34
webapp/tsconfig.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
, "server/src/index.cts" ],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||