Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 22 additions & 20 deletions .github/workflows/build-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ name: Build Reusable Steps
on:
workflow_call:
outputs:
build_output: # Changed from build-output to build_output for consistency
build_output: # Changed from build-output to build_output for consistency
description: "Build output artifact"
value: ${{ jobs.build.outputs.build_output }}
jobs:
build:
name: "Build Application"
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_CONVEX_URL: ${{ secrets.NEXT_PUBLIC_CONVEX_URL }}
CONVEX_DEPLOYMENT: ${{ secrets.CONVEX_DEPLOYMENT }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
LIVE_BLOCK_SECRET_API_KEY: ${{ secrets.LIVE_BLOCK_SECRET_API_KEY }}
outputs:
build_output: ${{ steps.build_step.outputs.result }}

Expand Down Expand Up @@ -55,32 +61,28 @@ jobs:
pnpm install
fi
- name: Build
id: build_step # Added this ID which is referenced in the outputs
id: build_step # Added this ID which is referenced in the outputs
env:
NEXT_PUBLIC_CONVEX_URL: ${{ secrets.NEXT_PUBLIC_CONVEX_URL }}
CONVEX_DEPLOYMENT: ${{ secrets.CONVEX_DEPLOYMENT }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
LIVE_BLOCK_SECRET_API_KEY: ${{ secrets.LIVE_BLOCK_SECRET_API_KEY }}
run: |
# Create a clean .env file
rm -f .env
touch .env

# Add each environment variable to .env file
# Using printf to avoid issues with special characters
printf "NEXT_PUBLIC_CONVEX_URL=%s\n" "$NEXT_PUBLIC_CONVEX_URL" >> .env
printf "CONVEX_DEPLOYMENT=%s\n" "$CONVEX_DEPLOYMENT" >> .env
printf "CLERK_SECRET_KEY=%s\n" "$CLERK_SECRET_KEY" >> .env
printf "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=%s\n" "$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY" >> .env
printf "LIVE_BLOCK_SECRET_API_KEY=%s\n" "$LIVE_BLOCK_SECRET_API_KEY" >> .env


# Create .env file more efficiently (single operation)
cat > .env << EOL
NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL}
CONVEX_DEPLOYMENT=${CONVEX_DEPLOYMENT}
CLERK_SECRET_KEY=${CLERK_SECRET_KEY}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
LIVE_BLOCK_SECRET_API_KEY=${LIVE_BLOCK_SECRET_API_KEY}
EOL

# Run the build command
pnpm build
# Set output for the workflow
echo "result=success" >> $GITHUB_OUTPUT
pnpm build || exit 1 # Add error handling

# Set output for the workflow more securely
echo "result=success" >> "$GITHUB_OUTPUT"

- name: Upload build artifacts
uses: actions/upload-artifact@v4
Expand All @@ -92,4 +94,4 @@ jobs:
package.json
pnpm-lock.yaml
next.config.js
.env
.env
4 changes: 2 additions & 2 deletions liveblocks.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ declare global {
id: string;
info: {
// Example properties, for useSelf, useUser, useOthers, etc.
// name: string;
// avatar: string;
name: string;
avatar: string;
};
};

Expand Down
76 changes: 41 additions & 35 deletions src/app/api/auth/liveblocks/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { auth, currentUser } from "@clerk/nextjs/server";
// import { Liveblocks } from "@liveblocks/node";
// import { ConvexHttpClient } from "convex/browser";
// import { api } from "../../../../../convex/_generated/api";

// const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
// const liveblocks = new Liveblocks({
// secret: process.env.LIVE_BLOCK_SECRET_API_KEY!,
// });
import { Liveblocks } from "@liveblocks/node";
import { ConvexHttpClient } from "convex/browser";
import { api } from "../../../../../convex/_generated/api";

// Ensure these environment variables are defined in your GitHub Actions workflow
// Add error handling for missing environment variables
const convex = new ConvexHttpClient(
process.env.NEXT_PUBLIC_CONVEX_URL ??
(() => { throw new Error("NEXT_PUBLIC_CONVEX_URL environment variable is not defined") })()
);

const liveblocks = new Liveblocks({
secret: process.env.LIVE_BLOCK_SECRET_API_KEY ??
(() => { throw new Error("LIVE_BLOCK_SECRET_API_KEY environment variable is not defined") })()
});

export async function POST(req: Request) {
const { sessionClaims } = await auth();
Expand All @@ -16,31 +23,30 @@ export async function POST(req: Request) {
if (!user) return new Response("Unauthorized", { status: 401 });

const { room } = await req.json();
// const document = await convex.query(api.document.get, {
// id: room,
// ignoreAuth: true,
// });

// if (!document) return new Response("Unauthorized", { status: 401 });

// const isOwner = document.ownerId === user.id;
// const isOrgMember = !!(
// document.organizationId && document.organizationId === sessionClaims.org_id
// );

// if (!isOwner && !isOrgMember)
// return new Response("Unauthorized", { status: 401 });

// const session = liveblocks.prepareSession(user.id, {
// userInfo: {
// name:
// user.fullName ?? user.primaryEmailAddress?.emailAddress ?? "Anonymous",
// avatar: user.imageUrl,
// },
// });

// session.allow(room, session.FULL_ACCESS);
// const { body, status } = await session.authorize();
// return new Response(body, { status });
return new Response(room, { status: 200 });
const document = await convex.query(api.document.get, {
id: room,
ignoreAuth: true,
});

if (!document) return new Response("Unauthorized", { status: 401 });

const isOwner = document.ownerId === user.id;
const isOrgMember = !!(
document.organizationId && document.organizationId === sessionClaims.org_id
);

if (!isOwner && !isOrgMember)
return new Response("Unauthorized", { status: 401 });

const session = liveblocks.prepareSession(user.id, {
userInfo: {
name:
user.fullName ?? user.primaryEmailAddress?.emailAddress ?? "Anonymous",
avatar: user.imageUrl,
},
});

session.allow(room, session.FULL_ACCESS);
const { body, status } = await session.authorize();
return new Response(body, { status });
}
22 changes: 22 additions & 0 deletions src/app/document/[documentId]/avatars-stack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { useOthers, useSelf } from "@liveblocks/react/suspense";
import { Avatars } from "./avatars";

export const AvatarsStack: React.FC = () => {
const users = useOthers();
const currentUser = useSelf();

if (!users.length) return <></>;
return (
<>
<div className="items-center">
{currentUser && (
<div className="relative ml-2">
<Avatars name="You" src={currentUser.info.avatar} />
</div>
)}
</div>
</>
);
};
25 changes: 25 additions & 0 deletions src/app/document/[documentId]/avatars.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Image from "next/image";
import React from "react";

const SIZE = 36;

type Props = {
src: string;
name: string;
};

export const Avatars: React.FC<Props> = ({ src, name }) => {
return (
<>
<div
style={{ width: SIZE, height: SIZE }}
className="group -ml-2 flex shrink-0 place-content-center relative border-4 border-white rounded-full bg-gray-400"
>
<div className="opacity-0 group-hover:opacity-100 absolute top-full py-1 px-2 text-white text-xs rounded-lg mt-2.5 z-10 bg-black whitespace-nowrap transition-opacity">
{name}
</div>
<Image src={src} alt={name} className="size-full rounded-full" />
</div>
</>
);
};
24 changes: 11 additions & 13 deletions src/app/document/[documentId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Navbar } from "./(navbar)/navbar";
import { Toolbar } from "./(toolbar)/toolbar";
import { Editor } from "./editor";
// import { Room } from "./room";
import { Room } from "./room";

interface Props {
params: Promise<{ documentId: string }>;
Expand All @@ -10,22 +10,20 @@ const Page: React.FC<Props> = async ({ params }) => {
const { documentId } = await params;
console.info(":Document:", documentId);

// <Room>
return (
<div className="min-h-screen bg-[#fafbfd]">
<div className="flex flex-col px-4 pt-2 gap-y-2 fixed top-0 left-0 right-0 z-10 bg-[#fafbfd] print:hidden">
<Navbar />
<Toolbar />
</div>
<Room>
<div className="min-h-screen bg-[#fafbfd]">
<div className="flex flex-col px-4 pt-2 gap-y-2 fixed top-0 left-0 right-0 z-10 bg-[#fafbfd] print:hidden">
<Navbar />
<Toolbar />
</div>

<div className="pt-[114px] print:pt-0">
<Editor />
<div className="pt-[114px] print:pt-0">
<Editor />
</div>
</div>
</div>
</Room>
);
{
/* </Room> */
}
};

export default Page;
124 changes: 61 additions & 63 deletions src/app/document/[documentId]/room.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,73 @@
"use client";

import { ReactNode } from "react";
import { FullscreenLoader } from "@/components/fullscreen-loader";
import { useToast } from "@/hooks/use-toast";
import {
ClientSideSuspense,
LiveblocksProvider,
RoomProvider,
} from "@liveblocks/react/suspense";
import { useParams } from "next/navigation";
import { ReactNode, useEffect, useMemo, useState } from "react";
import { getUsers } from "./action";

// import { FullscreenLoader } from "@/components/fullscreen-loader";
// import { useToast } from "@/hooks/use-toast";
// import {
// ClientSideSuspense,
// LiveblocksProvider,
// RoomProvider,
// } from "@liveblocks/react/suspense";
// import { useParams } from "next/navigation";
// import { ReactNode, useEffect, useMemo, useState } from "react";
// import { getUsers } from "./action";

// type User = {
// id: string;
// name: string;
// avatar: string;
// };
type User = {
id: string;
name: string;
avatar: string;
};

export function Room({ children }: { children: ReactNode }) {
// const params = useParams<{ documentId: string }>();
// const { toast } = useToast();
const params = useParams<{ documentId: string }>();
const { toast } = useToast();

// const [users, setUsers] = useState<User[]>([]);
const [users, setUsers] = useState<User[]>([]);

// const fetchUsers = useMemo(
// () => async () => {
// try {
// const users = await getUsers();
// setUsers(users);
// } catch {
// toast({
// title: "Error",
// description: "Failed to fetch users",
// variant: "destructive",
// });
// }
// },
// [toast]
// );
const fetchUsers = useMemo(
() => async () => {
try {
const users = await getUsers();
setUsers(users);
} catch {
toast({
title: "Error",
description: "Failed to fetch users",
variant: "destructive",
});
}
},
[toast]
);

// useEffect(() => {
// fetchUsers();
// }, [fetchUsers]);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);

return (
// <LiveblocksProvider
// authEndpoint={"/api/auth/liveblocks"}
// throttle={16}
// resolveUsers={({ userIds }) =>
// userIds.map((id) => users.find((u) => u.id === id))
// }
// resolveMentionSuggestions={({ text }) => {
// let filterUsers = users;
// if (text) {
// filterUsers = users.filter((u) =>
// u.name.toLowerCase().includes(text.toLowerCase())
// );
// }
// return filterUsers.map((u) => u.id);
// }}
// resolveRoomsInfo={() => []}
// >
// <RoomProvider id={params.documentId}>
// <ClientSideSuspense
// fallback={<FullscreenLoader label="Document loading..." />}
// >
<>{children}</>
// </ClientSideSuspense>
// </RoomProvider>
// </LiveblocksProvider>
<LiveblocksProvider
authEndpoint={"/api/auth/liveblocks"}
throttle={16}
resolveUsers={({ userIds }) =>
userIds.map((id) => users.find((u) => u.id === id))
}
resolveMentionSuggestions={({ text }) => {
let filterUsers = users;
if (text) {
filterUsers = users.filter((u) =>
u.name.toLowerCase().includes(text.toLowerCase())
);
}
return filterUsers.map((u) => u.id);
}}
resolveRoomsInfo={() => []}
>
<RoomProvider id={params.documentId}>
<ClientSideSuspense
fallback={<FullscreenLoader label="Document loading..." />}
>
{children}
</ClientSideSuspense>
</RoomProvider>
</LiveblocksProvider>
);
}
Loading