Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7da454d
fix: added test page for testing vite migration and also migrated few…
Mian-Zaid May 29, 2025
4609bf4
fix: migarted few more components
Mian-Zaid May 30, 2025
d785e32
fix: attenddes list
Mian-Zaid Jun 3, 2025
0cc32c9
fix: meeting body
Mian-Zaid Jun 5, 2025
4ca0f39
fix: host connection and display on it's video
Mian-Zaid Jun 10, 2025
6f75a2d
fix: role and name from query params
Mian-Zaid Jun 12, 2025
8bad44e
fix: share screen stream status and attendes width
Mian-Zaid Jun 12, 2025
d5381fd
device size
Mian-Zaid Jun 12, 2025
d9d7077
fix: video display and audience connection
Mian-Zaid Jun 16, 2025
afd4f2d
fix: invite aud to stage
Mian-Zaid Jun 17, 2025
77a1154
fix: Info dialog icon
Mian-Zaid Jun 17, 2025
ef0d1db
fix: muted status
Mian-Zaid Jun 18, 2025
b0af229
fix: host page
Mian-Zaid Jun 19, 2025
c8bce9d
fix: added audience page
Mian-Zaid Jun 20, 2025
638a886
fix: full screen
Mian-Zaid Jun 20, 2025
6d4f2ca
fix: profile dialog
Mian-Zaid Jun 24, 2025
b33ca72
fix: host dialogs
Mian-Zaid Jun 24, 2025
cd11fcf
fix: loading and redirection
Mian-Zaid Jun 24, 2025
5d95edf
fix: host path
Mian-Zaid Jun 24, 2025
814db45
disable video background for now
Mian-Zaid Jun 24, 2025
afc1743
fix: videos disply in mobile view
Mian-Zaid Jun 29, 2025
d39229a
fix: audience link
Mian-Zaid Jun 30, 2025
0eb15ee
fix: preview dialog
Mian-Zaid Jun 30, 2025
9131bab
fix: router
Mian-Zaid Jul 1, 2025
11e9aa9
fix: io devices dialog
Mian-Zaid Jul 1, 2025
86daa7e
fix: video background list
Mian-Zaid Jul 1, 2025
da8b44a
fix: video background
Mian-Zaid Jul 7, 2025
13ec0f1
fix: migration UI issues
Narixius Jul 16, 2025
fa2dd62
feat: disable canvas preview on start recording
Narixius Jul 22, 2025
f79d7d3
feat: implement DynamicLayout component for responsive video layout
Narixius Aug 12, 2025
fe42283
feat: add reactive layout utility for video conference streams
Narixius Aug 12, 2025
8a877a8
fix: remove unused import and adjust styling for VideoCard and Video …
Narixius Aug 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions apps/littleape/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
{
"plugins": [
"prettier"
],
"extends": [
"next/core-web-vitals",
"prettier"
],
"rules": {
"plugins": [],
"extends": [
"next/core-web-vitals"
],
"rules": {
"prettier/prettier": "error",
"no-deprecated-methods": "off"
"no-deprecated-methods": "off"
}
}
262 changes: 262 additions & 0 deletions apps/littleape/components/DyanmicLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import type { FC, PropsWithChildren, ReactElement } from "react";
import { cloneElement, isValidElement } from "react";

// Default aspect ratio for video (16:9)
const DEFAULT_ASPECT_RATIO = 16 / 9;

interface DynamicLayoutProps {
width: number;
height: number;
aspectRatio?: number; // Optional, defaults to 16/9
gap?: number; // Gap between items in px
pinnedIndex?: number;
pinnedRatio?: number; // Ratio of space for pinned video (0 < pinnedRatio < 1)
}

type LayoutChild = ReactElement<{
className?: string;
style?: React.CSSProperties;
}>;

export const DynamicLayout: FC<PropsWithChildren<DynamicLayoutProps>> = ({
width,
height,
aspectRatio = DEFAULT_ASPECT_RATIO,
gap = 0,
pinnedIndex = -1,
pinnedRatio = 9 / 12,
children,
}) => {
const childArray = Array.isArray(children) ? children : [children];
const count = childArray.length;

// Layout for regular grid (no pin)
let bestGrid = { area: 0, rows: 1, cols: count, w: 0, h: 0 };
for (let rows = 1; rows <= count; rows++) {
const cols = Math.ceil(count / rows);
const totalGapW = gap * (cols - 1);
const totalGapH = gap * (rows - 1);
let cellW = (width - totalGapW) / cols;
let cellH = (height - totalGapH) / rows;
if (cellW / cellH > aspectRatio) {
cellW = cellH * aspectRatio;
} else {
cellH = cellW / aspectRatio;
}
const area = cellW * cellH;
if (area > bestGrid.area) {
bestGrid = { area, rows, cols, w: cellW, h: cellH };
}
}
const totalW = bestGrid.cols * bestGrid.w + gap * (bestGrid.cols - 1);
const totalH = bestGrid.rows * bestGrid.h + gap * (bestGrid.rows - 1);
const offsetX = (width - totalW) / 2;
const offsetY = (height - totalH) / 2;

// Layout for pin mode
let pinLayout: { [idx: number]: React.CSSProperties } = {};
if (pinnedIndex !== -1 && count > 1) {
// Step 1: Calculate initial split based on pinnedRatio
const splitVertical = width >= height;
const others = childArray.filter((_, i) => i !== pinnedIndex);
const minPinnedSize = splitVertical ? width * pinnedRatio : height * pinnedRatio;
const minOthersSize = splitVertical ? width * (1 - pinnedRatio) : height * (1 - pinnedRatio);
let pinnedSize = minPinnedSize;
let othersSize = minOthersSize;
let othersBox = splitVertical
? {
left: pinnedSize + gap,
top: 0,
width: othersSize - gap,
height,
}
: {
left: 0,
top: pinnedSize + gap,
width,
height: othersSize - gap,
};
let best = { area: 0, rows: 1, cols: others.length, w: 0, h: 0 };
// Step 2: Layout unpinned videos in their section
for (let rows = 1; rows <= others.length; rows++) {
const cols = Math.ceil(others.length / rows);
const totalGapW = gap * (cols - 1);
const totalGapH = gap * (rows - 1);
let cellW = (othersBox.width - totalGapW) / cols;
let cellH = (othersBox.height - totalGapH) / rows;
if (cellW / cellH > aspectRatio) {
cellW = cellH * aspectRatio;
} else {
cellH = cellW / aspectRatio;
}
const area = cellW * cellH;
if (area > best.area) {
best = { area, rows, cols, w: cellW, h: cellH };
}
}
// Step 3: Check for leftover space in unpinned section
const usedOthersWidth = splitVertical
? best.cols * best.w + gap * (best.cols - 1)
: othersBox.width;
const usedOthersHeight = splitVertical
? othersBox.height
: best.rows * best.h + gap * (best.rows - 1);
let leftover = splitVertical
? othersBox.width - usedOthersWidth
: othersBox.height - usedOthersHeight;
if (leftover > 0) {
// Grow pinned video to fill leftover space
if (splitVertical) {
pinnedSize += leftover;
othersBox = {
left: pinnedSize + gap / 2,
top: 0,
width: width - pinnedSize - gap / 2,
height,
};
} else {
pinnedSize += leftover;
othersBox = {
left: 0,
top: pinnedSize + gap / 2,
width,
height: height - pinnedSize - gap / 2,
};
}
}
// Recalculate pinned/unpinned layout with new pinnedSize
const pinnedBox = splitVertical
? { left: 0, top: 0, width: pinnedSize, height }
: { left: 0, top: 0, width, height: pinnedSize };
const totalW2 = best.cols * best.w + gap * (best.cols - 1);
const totalH2 = best.rows * best.h + gap * (best.rows - 1);
const offsetX2 = othersBox.left + (othersBox.width - totalW2) / 2;
const offsetY2 = othersBox.top + (othersBox.height - totalH2) / 2;
// Pinned
pinLayout[pinnedIndex] = {
position: "absolute",
left: pinnedBox.left,
top: pinnedBox.top,
width: pinnedBox.width,
height: pinnedBox.height,
zIndex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
overflow: "hidden",
};
// Others
let k = 0;
for (let i = 0; i < count; i++) {
if (i === pinnedIndex) continue;
const row = Math.floor(k / best.cols);
const col = k % best.cols;
// Center last row if not full
let left = offsetX2 + col * (best.w + gap);
if (row === best.rows - 1) {
const itemsInLastRow = others.length - best.cols * (best.rows - 1);
if (itemsInLastRow < best.cols) {
const extraSpace = ((best.cols - itemsInLastRow) * (best.w + gap)) / 2;
left += extraSpace;
}
}
pinLayout[i] = {
position: "absolute",
left,
top: offsetY2 + row * (best.h + gap),
width: best.w,
height: best.h,
zIndex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
overflow: "hidden",
};
k++;
}
} else if (pinnedIndex !== -1 && count === 1) {
// Single pinned: full size, others hidden
pinLayout[0] = {
position: "absolute",
left: 0,
top: 0,
width,
height,
zIndex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
overflow: "hidden",
};
}

return (
<div
style={{
width,
height,
}}
className="relative overflow-hidden transition-all mt-4 ml-4"
>
{childArray.map((child, i) => {
if (!isValidElement(child)) return null;
const el = child as LayoutChild;
let style: React.CSSProperties;
if (pinnedIndex !== -1) {
if (pinLayout[i]) {
style = { ...pinLayout[i], ...(el.props.style || {}) };
} else if (count === 1) {
// Shouldn't happen, but fallback
style = {
opacity: 0,
pointerEvents: "none",
zIndex: 0,
width: 0,
height: 0,
...(el.props.style || {}),
};
} else {
// Hide non-pinned in single-pinned case
style = {
opacity: 0,
pointerEvents: "none",
zIndex: 0,
width: 0,
height: 0,
...(el.props.style || {}),
};
}
} else {
// Regular grid
const row = Math.floor(i / bestGrid.cols);
const col = i % bestGrid.cols;
let left = offsetX + col * (bestGrid.w + gap);
if (row === bestGrid.rows - 1) {
const itemsInLastRow = count - bestGrid.cols * (bestGrid.rows - 1);
if (itemsInLastRow < bestGrid.cols) {
const extraSpace = ((bestGrid.cols - itemsInLastRow) * (bestGrid.w + gap)) / 2;
left += extraSpace;
}
}
style = {
position: "absolute" as const,
left,
top: offsetY + row * (bestGrid.h + gap),
width: bestGrid.w,
height: bestGrid.h,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
overflow: "hidden",
...(el.props.style || {}),
};
}
return cloneElement(el, { style, key: i });
})}
</div>
);
};
68 changes: 42 additions & 26 deletions apps/littleape/components/LinkCopyComponent/index.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
import Icon from "components/Icon"
import CopyIcon from '../../public/Copy.svg'
import LinkIcon from '../../public/Link.svg'
import logger from '../../lib/logger/logger'
import Icon from "components/Icon";
import CopyIcon from "../../public/Copy.svg";
import LinkIcon from "../../public/Link.svg";
import logger from "../../lib/logger/logger";
import clsx from "clsx";
import { makeDialog } from "components/vite-migrated/Dialog";

export const LinkCopyComponent = ({ title, link }) => {
const copyToClipboard = () => {
navigator.clipboard.writeText(link).then(() => {
logger.log("Copied to clipboard");
}).catch((err) => {
logger.error("Failed to copy:", err);
});
};
type Props = {
link: string;
title?: string;
className?: string;
};

return (
<div className="flex flex-col gap-1 w-full max-w-[274px]">
{title && <span className="text-bold-12 text-gray-3">{title}</span>}
export const LinkCopyComponent = ({ title, link, className }: Props) => {
const copyToClipboard = () => {
navigator.clipboard
.writeText(link)
.then(() => {
makeDialog("info", {
message: "Meeting link copied to clipboard",
icon: "Check",
});
})
.catch((err) => {
makeDialog("info", {
message: "Failed to copy meeting link",
icon: "Close",
});
});
};

<div className="w-full bg-gray-0 px-4 py-2 text-gray-2 flex justify-between rounded-full items-center">
<Icon icon={LinkIcon} />
return (
<div className={clsx("flex flex-col gap-1 w-full max-w-[274px]", className)}>
{title && <span className="text-bold-12 text-gray-3">{title}</span>}

<div className="flex-1 flex gap-2 items-center overflow-hidden min-w-0 mx-2">
<span className="text-medium-12 truncate greatape-meeting-link">{link}</span>
</div>
<div className="w-full bg-gray-0 px-4 py-2 text-gray-2 flex justify-between rounded-full items-center">
<Icon icon={LinkIcon} />

<button className="cursor-pointer shrink-0" onClick={copyToClipboard}>
<Icon icon={CopyIcon} />
</button>
</div>
<div className="flex-1 flex gap-2 items-center overflow-hidden min-w-0 mx-2">
<span className="text-medium-12 truncate greatape-meeting-link">{link}</span>
</div>

)
}
<button className="cursor-pointer shrink-0" onClick={copyToClipboard}>
<Icon icon={CopyIcon} />
</button>
</div>
</div>
);
};
Loading